Added LDAP config and auth
This commit is contained in:
Generated
+292
-3
@@ -11,6 +11,7 @@
|
||||
"@chakra-ui/react": "^3.22.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"axios": "^1.18.1",
|
||||
"framer-motion": "^12.23.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.539.0",
|
||||
@@ -4943,6 +4944,18 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -5090,6 +5103,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
|
||||
@@ -5100,6 +5119,18 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
@@ -5236,6 +5267,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -5550,6 +5594,18 @@
|
||||
"integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@@ -5955,6 +6011,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@@ -6011,6 +6076,20 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dup": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz",
|
||||
@@ -6090,6 +6169,24 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||
@@ -6097,6 +6194,33 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es5-ext": {
|
||||
"version": "0.10.64",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||
@@ -6663,6 +6787,26 @@
|
||||
"dtype": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/font-atlas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/font-atlas/-/font-atlas-2.1.0.tgz",
|
||||
@@ -6697,6 +6841,22 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz",
|
||||
@@ -6787,6 +6947,43 @@
|
||||
"integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -7170,6 +7367,18 @@
|
||||
"resolve": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -7219,10 +7428,38 @@
|
||||
"is-browser": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -7263,6 +7500,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -7966,6 +8216,15 @@
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/math-log2": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz",
|
||||
@@ -7975,6 +8234,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
@@ -8859,6 +9139,15 @@
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-memoize": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-memoize/-/proxy-memoize-3.0.1.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@chakra-ui/react": "^3.22.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"axios": "^1.18.1",
|
||||
"framer-motion": "^12.23.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.539.0",
|
||||
|
||||
+39
-24
@@ -1,7 +1,13 @@
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useEffect } from 'react';
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
import { useRecipe, Grid, GridItem, useDialog, Flex, Heading } from "@chakra-ui/react";
|
||||
import { createContext } from "react";
|
||||
|
||||
// --- NEW AUTH IMPORTS ---
|
||||
import useAuthStore from './store/authStore';
|
||||
import ProtectedRoute from './components/layout/ProtectedRoute.jsx';
|
||||
|
||||
// Pages & Components
|
||||
import Login from "@/pages/auth/Login.jsx";
|
||||
import Sidebar from "./components/layout/Sidebar.jsx";
|
||||
import DialogPopup from "@/components/dashboard/DialogPopup.jsx";
|
||||
@@ -41,43 +47,52 @@ function AppFoundation() {
|
||||
)
|
||||
}
|
||||
|
||||
// function ProtectedRoute({ user, children }) {
|
||||
// if (user) return children;
|
||||
// else return <Navigate to="/login" replace />;
|
||||
// }
|
||||
const Placeholder = ({ name }) => (
|
||||
<Flex width="100%" height="100%" align="center" justify="center" bg="gray.50"_dark={{ bg: "gray.900" }}>
|
||||
<Flex width="100%" height="100%" align="center" justify="center" bg="gray.50" _dark={{ bg: "gray.900" }}>
|
||||
<Heading color="gray.400" _dark={{ bg: "gray.600" }} size="md">{name} Dashboard - Coming Soon</Heading>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
function App() {
|
||||
// 1. Pull the initialization function from Zustand
|
||||
const checkAuth = useAuthStore((state) => state.checkAuth);
|
||||
|
||||
// 2. Ping Django to verify the HttpOnly cookie the moment the app boots
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{/*Public Routes*/}
|
||||
{/* --- Public Routes --- */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/*Non-Public Routes*/}
|
||||
<Route element={<AppFoundation />}>
|
||||
{/* --- SECURE ROUTES --- */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
|
||||
<Route path="/home" element={<Home />}>
|
||||
{/* We use our "pass" component to fill the Outlet until the real pages are built */}
|
||||
<Route index element={<MyDashboard />} />
|
||||
<Route path="gas" element={<MyDashboard />} />
|
||||
<Route path="seismic" element={<Placeholder name="Seismic" />} />
|
||||
<Route path="remote" element={<Placeholder name="Remote Sensing" />} />
|
||||
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
|
||||
{/* --- Non-Public Routes (Now completely locked down) --- */}
|
||||
<Route element={<AppFoundation />}>
|
||||
|
||||
<Route path="/home" element={<Home />}>
|
||||
<Route index element={<MyDashboard />} />
|
||||
<Route path="gas" element={<MyDashboard />} />
|
||||
<Route path="seismic" element={<Placeholder name="Seismic" />} />
|
||||
<Route path="remote" element={<Placeholder name="Remote Sensing" />} />
|
||||
<Route path="daily" element={<Placeholder name="Daily Activity" />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/global-map" element={<GlobalMap />} />
|
||||
<Route path="/regional-map" element={<RegionalMap />} />
|
||||
<Route path="/volcano" element={<Volcano />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/global-map" element={<GlobalMap />} />
|
||||
<Route path="/regional-map" element={<RegionalMap />} />
|
||||
<Route path="/volcano" element={<Volcano />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
</Route>
|
||||
|
||||
{/*Redirect Routes*/}
|
||||
{/* --- Redirect Routes --- */}
|
||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
@@ -0,0 +1,17 @@
|
||||
// src/api/axiosConfig.js
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
// Vite will catch this relative path and proxy it to Django
|
||||
baseURL: '/api/v3/',
|
||||
|
||||
withCredentials: true,
|
||||
xsrfCookieName: 'csrftoken',
|
||||
xsrfHeaderName: 'X-CSRFToken',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
export default api;
|
||||
@@ -59,12 +59,12 @@ export default function VaaActivity7Day() {
|
||||
paper_bgcolor: 'transparent', // Let Chakra UI background show through
|
||||
plot_bgcolor: 'transparent',
|
||||
font: { color: '#d1d5db' }, // gray.300
|
||||
// margin: { t: 40, r: 20, l: 30, b: 60 }, // Extra bottom margin for legend
|
||||
title: {
|
||||
text: '7 Day Consolidated Flight Levels',
|
||||
font: { size: 16, color: 'white' },
|
||||
x: 0.05
|
||||
},
|
||||
margin: { t: 20, r: 20, l: 20, b: 20 },
|
||||
// title: {
|
||||
// text: '7 Day Consolidated Flight Levels',
|
||||
// font: { size: 16, color: 'white' },
|
||||
// x: 0.05
|
||||
// },
|
||||
xaxis: {
|
||||
title: {
|
||||
text: 'Date (UTC)',
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useRecipe, Button, Stack, Text, Flex } from "@chakra-ui/react"
|
||||
import UserAvatarMenu from "./UserAvatarMenu.jsx";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import useAuthStore from "@/store/authStore";
|
||||
|
||||
// Import the asset properly so Vite can bundle it for any server
|
||||
// import defaultAvatar from "@/assets/user_profile.svg";
|
||||
|
||||
// TODO: Pull this dynamically from an Auth Context later!
|
||||
const placeholderUser = {
|
||||
name: "Princess Zelda",
|
||||
email: "zelda.royal@hyrule.gov",
|
||||
// avatar: defaultAvatar
|
||||
}
|
||||
|
||||
// Accept a 'user' prop now, fallback to placeholder if empty
|
||||
export default function Header({ user = placeholderUser }) {
|
||||
export default function Header() {
|
||||
const recipe = useRecipe({ key: "header" });
|
||||
const styles = recipe();
|
||||
|
||||
const { user } = useAuthStore();
|
||||
console.log(user);
|
||||
const displayName = user ? (user.fullName || user.username) : "Loading...";
|
||||
|
||||
const displayEmail = user?.email || "LDAP User";
|
||||
|
||||
return (
|
||||
<Flex css={styles} justify="space-between" align="center" width="100%">
|
||||
{/* Home Link Logo */}
|
||||
@@ -27,12 +27,12 @@ export default function Header({ user = placeholderUser }) {
|
||||
{/* User Profile Section */}
|
||||
<Flex align="center" gap={3} pr={6}>
|
||||
<Stack gap="0" textAlign="right" cursor="default">
|
||||
<Text color="color" fontWeight="bold" textStyle="lg">{user.name}</Text>
|
||||
<Text color="fg.muted" textStyle="sm">{user.email}</Text>
|
||||
<Text color="color" fontWeight="bold" textStyle="lg">{displayName}</Text>
|
||||
<Text color="fg.muted" textStyle="sm">{displayEmail}</Text>
|
||||
</Stack>
|
||||
<UserAvatarMenu
|
||||
name={user.name}
|
||||
avatar={user.avatar}
|
||||
name={displayName}
|
||||
// avatar={user.avatar}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import useAuthStore from '../../store/authStore';
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
// Grab the state directly from your Zustand store
|
||||
const { isAuthenticated, isLoading } = useAuthStore();
|
||||
|
||||
// 1. App is currently pinging Django to verify the HttpOnly cookie
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '20vh' }}>
|
||||
<h2>Loading Secure Environment...</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. The check failed (no cookie or expired cookie)
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// 3. The check passed! Render the layout shell and the protected children
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Avatar, Menu, Portal, Box } from "@chakra-ui/react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
import useAuthStore from "../../store/authStore";
|
||||
|
||||
export default function UserAvatarMenu({ name, avatar }) {
|
||||
const { logout } = useAuthStore();
|
||||
|
||||
return (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger focusRing="none">
|
||||
<Box
|
||||
rounded="full"
|
||||
tabIndex={0}
|
||||
_hover={{ boxShadow: "0 0 0 2px rgb(102, 122, 182)" }}
|
||||
cursor="pointer"
|
||||
rounded="full"
|
||||
tabIndex={0}
|
||||
_hover={{ boxShadow: "0 0 0 2px rgb(102, 122, 182)" }}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Avatar.Root shape="full">
|
||||
<Avatar.Fallback name={name} />
|
||||
@@ -25,12 +27,19 @@ export default function UserAvatarMenu({ name, avatar }) {
|
||||
<Menu.Item value="settings" as={NavLink} to={"/settings"}>
|
||||
Settings
|
||||
</Menu.Item>
|
||||
<Menu.Item value="logout" onClick={() => console.log("Log out triggered")}>
|
||||
|
||||
{/* Replaced console.log with the Zustand logout action */}
|
||||
<Menu.Item
|
||||
value="logout"
|
||||
onClick={logout}
|
||||
color="red.600"
|
||||
_dark={{ color: "red.400" }}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Content>
|
||||
</Menu.Positioner>
|
||||
</Portal>
|
||||
</Menu.Root>
|
||||
);
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
// config/widgetRegistry.js
|
||||
import { lazy } from 'react';
|
||||
|
||||
// We assume a 12-column grid.
|
||||
// w: 12 = full screen width
|
||||
@@ -11,6 +12,7 @@ export const WIDGET_REGISTRY = {
|
||||
name: "VAA 7 Day Activity",
|
||||
category: "Aviation",
|
||||
description: "Displays 7-day flight level timelines across multiple volcanoes.",
|
||||
component: lazy(() => import('../components/widgets/VaaActivity7Day')),
|
||||
allowedSizes: {
|
||||
xlarge: { w: 12, h: 12 },
|
||||
}
|
||||
|
||||
+123
-80
@@ -1,91 +1,134 @@
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
Stack,
|
||||
useRecipe,
|
||||
Center,
|
||||
Text,
|
||||
Flex,
|
||||
Heading,
|
||||
Highlight,
|
||||
GridItem,
|
||||
Grid
|
||||
} from "@chakra-ui/react"
|
||||
import { PasswordInput } from "@/components/ui/password-input.jsx";
|
||||
Box, Heading, Stack, Input, Button, Text, Separator, Field, Link, Flex
|
||||
} from '@chakra-ui/react';
|
||||
import useAuthStore from '../../store/authStore';
|
||||
import { toaster } from "../../components/ui/toaster.jsx"; // Ensure this path matches your project
|
||||
|
||||
export default function Login( signedIn, setSignedIn) {
|
||||
const recipe = useRecipe({ key: "dashboard" }); // styling recipes option
|
||||
const styles = recipe(); // use style
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitSuccessful },
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const { login, isAuthenticated, isLoading, error } = useAuthStore();
|
||||
|
||||
// --- VDAP COLOR STYLES ---
|
||||
const fieldStyles = {
|
||||
borderRadius: "3xl",
|
||||
bg: "gray.100",
|
||||
borderColor: "gray.100",
|
||||
px: 4,
|
||||
_focus: {
|
||||
bg: "white",
|
||||
borderColor: "blue.400",
|
||||
boxShadow: "0 0 0 1px #4299E1",
|
||||
outline: "none"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (data) => {
|
||||
console.log(data);
|
||||
// setUser(data.username);
|
||||
const buttonStyles = {
|
||||
borderRadius: "3xl",
|
||||
bg: "blue.600",
|
||||
color: "white",
|
||||
_hover: { bg: 'blue.700', transform: 'translateY(-1px)' },
|
||||
_active: { bg: 'blue.800', transform: 'translateY(0)' },
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
// The Redirect Watcher
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate('/home', { replace: true });
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
// The Error Watcher
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toaster.create({
|
||||
title: "Login Failed",
|
||||
description: error,
|
||||
type: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (username && password) {
|
||||
await login(username, password);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex css={styles} width="100%" height="100%">
|
||||
<Grid
|
||||
templateRows="1fr 11fr"
|
||||
height="100vh"
|
||||
width="100vw"
|
||||
>
|
||||
<GridItem rowSpan={1} >
|
||||
<Flex width="100%" height="100%" align="center" >
|
||||
<Heading size="4xl" fontWeight="bold" pl="1rem" cursor="default" >
|
||||
<Highlight query="A" styles={{ color: 'red.600'}}>
|
||||
VDAP
|
||||
</Highlight>
|
||||
</Heading>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
<GridItem rowSpan={11} >
|
||||
<Flex width="100%" height="100%" justify="center" align="center" direction="column" >
|
||||
<Center background="white" width="40%" height="50%" borderRadius="1rem">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="4" align="center" maxW="sm">
|
||||
<Text textStyle="2xl" fontWeight="bold" cursor="default">Sign In</Text>
|
||||
<Field.Root invalid={!!errors.username}>
|
||||
<Input
|
||||
placeholder="username"
|
||||
{...register("username", {required: "Required"})}
|
||||
/>
|
||||
<Field.ErrorText>{errors.username?.message}</Field.ErrorText>
|
||||
</Field.Root>
|
||||
<Flex p={8} bg="gray.50" _dark={{ bg: "gray.900" }} flex="1" minH="100vh" align="center" justify="center">
|
||||
<Box w="full" maxW="450px" bg="white" _dark={{ bg: "gray.800", borderColor: "gray.700" }} p={8} borderRadius="2xl" boxShadow="sm" borderWidth="1px" borderColor="gray.200">
|
||||
<Stack gap={6} align="stretch" as="form" onSubmit={handleSubmit}>
|
||||
|
||||
<Field.Root invalid={!!errors.password}>
|
||||
<PasswordInput
|
||||
placeholder="password"
|
||||
{...register("password", {required: "Required"})}
|
||||
/>
|
||||
<Field.ErrorText>{errors.password?.message}</Field.ErrorText>
|
||||
</Field.Root>
|
||||
<Stack gap={2} textAlign="center">
|
||||
<Heading as="h1" size="xl" color="gray.800" _dark={{ color: "white" }}>
|
||||
VDAP System Login
|
||||
</Heading>
|
||||
<Text color="gray.500">Sign in with your LDAP credentials.</Text>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitSuccessful}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Center>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Flex>
|
||||
<Separator />
|
||||
|
||||
<Field.Root required>
|
||||
<Field.Label ml={1} fontSize="sm" fontWeight="medium" color="gray.700" _dark={{ color: "gray.300" }}>
|
||||
Username
|
||||
</Field.Label>
|
||||
<Input
|
||||
name="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="jdoe"
|
||||
{...fieldStyles}
|
||||
/>
|
||||
</Field.Root>
|
||||
|
||||
<Field.Root required>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" w="full" mb={1} ml={1}>
|
||||
<Field.Label mb={0} fontSize="sm" fontWeight="medium" color="gray.700" _dark={{ color: "gray.300" }}>
|
||||
Password
|
||||
</Field.Label>
|
||||
</Box>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
{...fieldStyles}
|
||||
/>
|
||||
</Field.Root>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
loadingText="Authenticating..."
|
||||
size="lg"
|
||||
mt={2}
|
||||
w="full"
|
||||
{...buttonStyles}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<Text textAlign="center" fontSize="sm" color="gray.500">
|
||||
Need access?{' '}
|
||||
<Link href="mailto:it-chight@usgs.gov" color="blue.600" _dark={{ color: "blue.400" }} fontWeight="medium">
|
||||
Contact IT Administration
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,94 +1,146 @@
|
||||
import { Box, Text, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { Box, Text, Flex, IconButton, Spinner } from "@chakra-ui/react";
|
||||
import { X } from "lucide-react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { useDashboardStore } from "@/store/useDashboardStore.jsx";
|
||||
import { Suspense } from 'react';
|
||||
|
||||
// 2. Import registry!
|
||||
import { WIDGET_REGISTRY } from '@/constants/widgetRegistry.jsx';
|
||||
|
||||
// React-Grid-Layout requires this wrapper to automatically calculate screen width
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
export default function MyDashboard() {
|
||||
// 1. ☁️ Connect to the Cloud (Zustand)
|
||||
const widgets = useDashboardStore(state => state.widgets);
|
||||
const isEditing = useDashboardStore(state => state.isEditing);
|
||||
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||
const removeWidget = useDashboardStore(state => state.removeWidget);
|
||||
// 1. ☁️ Connect to the Cloud (Zustand)
|
||||
const widgets = useDashboardStore(state => state.widgets);
|
||||
const isEditing = useDashboardStore(state => state.isEditing);
|
||||
const updateLayoutPositions = useDashboardStore(state => state.updateLayoutPositions);
|
||||
const removeWidget = useDashboardStore(state => state.removeWidget);
|
||||
|
||||
// 2. 🛠️ Format the data for RGL
|
||||
// RGL expects a very specific array format for its layout prop
|
||||
const rglLayout = widgets.map(w => ({
|
||||
i: w.id,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
||||
}));
|
||||
// 2. 🛠️ Format the data for RGL
|
||||
// RGL expects a very specific array format for its layout prop
|
||||
const rglLayout = widgets.map(w => ({
|
||||
i: w.id,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
isResizable: false // 🚫 The magic lock! No resizing allowed globally.
|
||||
}));
|
||||
|
||||
// 3. THE COMPONENT MAPPER
|
||||
const renderWidget = (widget) => {
|
||||
const registryEntry = WIDGET_REGISTRY[widget.type];
|
||||
|
||||
if (registryEntry && registryEntry.component) {
|
||||
const Component = registryEntry.component;
|
||||
|
||||
return (
|
||||
// The "Frame" that holds both the Title and the Chart
|
||||
<Flex direction="column" height="100%" width="100%" bg="gray.800">
|
||||
|
||||
{/* 1. THE TITLE BAR (Dynamically pulled from registry!) */}
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
px={3}
|
||||
py={2}
|
||||
// borderBottom="1px solid"
|
||||
// borderColor="gray.700"
|
||||
>
|
||||
<Text color="gray.100" fontWeight="semibold" fontSize="md" isTruncated>
|
||||
{registryEntry.name}
|
||||
</Text>
|
||||
|
||||
{/* (Optional) This is exactly where the ellipsis/settings icon will go later! */}
|
||||
<Box w="20px" h="20px" />
|
||||
</Flex>
|
||||
|
||||
{/* 2. THE CHART CANVAS */}
|
||||
<Box flex="1" overflow="hidden" position="relative" p={1}>
|
||||
<Component />
|
||||
</Box>
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%">
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={{ lg: rglLayout }} // We feed it the formatted layout map
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||
rowHeight={30}
|
||||
isDraggable={isEditing} // Only allows dragging when your Settings tools toggle this
|
||||
isResizable={false} // Double lock-down
|
||||
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||
compactType="vertical" // Auto-packs widgets to the top
|
||||
>
|
||||
{/* 3. 🎨 Paint the Widgets */}
|
||||
{widgets.map(widget => (
|
||||
<Box
|
||||
key={widget.id}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
boxShadow={isEditing ? "outline" : "sm"} // Visual cue when editing
|
||||
border={isEditing ? "2px dashed gray" : "1px solid"}
|
||||
borderColor={isEditing ? "gray.400" : "gray.200"}
|
||||
overflow="hidden"
|
||||
cursor={isEditing ? "grab" : "default"}
|
||||
position="relative" // 3. REQUIRED so the absolute button stays inside this box
|
||||
>
|
||||
{/* 4. THE DELETE BUTTON */}
|
||||
{isEditing && (
|
||||
<IconButton
|
||||
aria-label="Remove widget"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
size="xs"
|
||||
bg="white"
|
||||
color="gray.400"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }}
|
||||
zIndex={2}
|
||||
onClick={() => removeWidget(widget.id)}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
|
||||
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
{/* 🚧 Temporary Placeholder!
|
||||
Eventually, we will swap this block out for a dynamic component loader
|
||||
that looks at widget.type and loads the correct graph or table.
|
||||
*/}
|
||||
<Flex width="100%" height="100%" align="center" justify="center" direction="column">
|
||||
<Text fontWeight="bold" color="gray.600">{widget.type}</Text>
|
||||
<Text fontSize="sm" color="gray.400">Size: {widget.size}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Empty State Helper */}
|
||||
{widgets.length === 0 && (
|
||||
<Flex width="100%" height="200px" align="center" justify="center">
|
||||
<Text color="gray.400">Your dashboard is empty. Click Settings to add widgets.</Text>
|
||||
// Fallback if someone asks for a widget that doesn't exist
|
||||
return (
|
||||
<Flex width="100%" height="100%" align="center" justify="center" direction="column" bg="gray.800">
|
||||
<Text fontWeight="bold" color="red.400">Error</Text>
|
||||
<Text fontSize="sm" color="gray.400">Widget '{widget.type}' not found.</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%">
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={{ lg: rglLayout }} // We feed it the formatted layout map
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} // Our 12-column master grid
|
||||
rowHeight={30}
|
||||
isDraggable={isEditing} // Only allows dragging when your Settings tools toggle this
|
||||
isResizable={false} // Double lock-down
|
||||
onLayoutChange={(newLayout) => updateLayoutPositions(newLayout)} // Fires when user drops a widget
|
||||
compactType="vertical" // Auto-packs widgets to the top
|
||||
>
|
||||
{/* 3. 🎨 Paint the Widgets */}
|
||||
{widgets.map(widget => (
|
||||
<Box
|
||||
key={widget.id}
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
boxShadow={isEditing ? "outline" : "sm"} // Visual cue when editing
|
||||
border={isEditing ? "2px dashed gray" : "1px solid"}
|
||||
borderColor={isEditing ? "gray.400" : "gray.200"}
|
||||
overflow="hidden"
|
||||
cursor={isEditing ? "grab" : "default"}
|
||||
position="relative" // 3. REQUIRED so the absolute button stays inside this box
|
||||
>
|
||||
{/* 4. THE DELETE BUTTON */}
|
||||
{isEditing && (
|
||||
<IconButton
|
||||
aria-label="Remove widget"
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
size="xs"
|
||||
bg="white"
|
||||
color="gray.400"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
_hover={{ bg: "red.50", color: "red.500", borderColor: "red.200" }}
|
||||
zIndex={2}
|
||||
onClick={() => removeWidget(widget.id)}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 🚫 Stops RGL from grabbing the widget
|
||||
onTouchStart={(e) => e.stopPropagation()} // 🚫 Same protection for mobile touches
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* 4. SUSPENSE BOUNDARY wrapping the render function */}
|
||||
<Suspense fallback={
|
||||
<Flex w="100%" h="100%" align="center" justify="center" bg="gray.800">
|
||||
<Spinner size="md" color="blue.400" thickness="3px" />
|
||||
</Flex>
|
||||
}>
|
||||
{renderWidget(widget)}
|
||||
</Suspense>
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
|
||||
{/* Empty State Helper */}
|
||||
{widgets.length === 0 && (
|
||||
<Flex width="100%" height="200px" align="center" justify="center">
|
||||
<Text color="gray.400">Your dashboard is empty. Click Settings to add widgets.</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { create } from 'zustand';
|
||||
import api from '../api/axiosConfig';
|
||||
|
||||
const useAuthStore = create((set) => ({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
user: null,
|
||||
|
||||
login: async (username, password) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const res = await api.post('login/', { username, password });
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: {
|
||||
username: res.data.username,
|
||||
firstName: res.data.first_name,
|
||||
lastName: res.data.last_name,
|
||||
email: res.data.email,
|
||||
fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' ')
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// PROPER USAGE: Check if Django sent a specific error string, otherwise default
|
||||
const backendError = err.response?.data?.error || 'Invalid network credentials';
|
||||
set({ error: backendError, isLoading: false, isAuthenticated: false });
|
||||
}
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await api.post('logout/');
|
||||
} catch (err) {
|
||||
console.error("Logout error:", err);
|
||||
} finally {
|
||||
set({ isAuthenticated: false, user: null, error: null });
|
||||
}
|
||||
},
|
||||
|
||||
checkAuth: async () => {
|
||||
try {
|
||||
// Hit the new lightweight endpoint
|
||||
const res = await api.get('session');
|
||||
|
||||
// Rehydrate the store with the user's info!
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: {
|
||||
username: res.data.username,
|
||||
firstName: res.data.first_name,
|
||||
lastName: res.data.last_name,
|
||||
email: res.data.email,
|
||||
fullName: [res.data.first_name, res.data.last_name].filter(Boolean).join(' ')
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// If it fails, ensure the store is completely wiped
|
||||
set({ isAuthenticated: false, isLoading: false, user: null });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
export default useAuthStore;
|
||||
@@ -21,6 +21,7 @@ export default defineConfig({
|
||||
target: 'http://vdap.org:8082', // The remote internet API
|
||||
changeOrigin: true, // Tricks the remote server into accepting the request
|
||||
secure: false, // Set to true later when you upgrade to HTTPS
|
||||
cookieDomainRewrite: 'localhost',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user