diff --git a/istsos4-wizard/src/App.jsx b/istsos4-wizard/src/App.jsx index 65f20d2..d48454f 100644 --- a/istsos4-wizard/src/App.jsx +++ b/istsos4-wizard/src/App.jsx @@ -1,14 +1,20 @@ import React from 'react'; -import { WizardProvider, StepRenderer } from './components/wizard'; +import { WizardProvider } from './components/wizard'; +import StepRenderer from './components/wizard/StepRenderer'; import ProgressBar from './components/common/ProgressBar'; import Navigation from './components/common/Navigation'; +import SessionRecovery from './components/common/SessionRecovery'; function App() { return ( -
-
-
+
+ +
+
+

+ IstSOS4 Configuration Wizard +

@@ -19,4 +25,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/istsos4-wizard/src/components/common/FormField.jsx b/istsos4-wizard/src/components/common/FormField.jsx index 146b160..d687007 100644 --- a/istsos4-wizard/src/components/common/FormField.jsx +++ b/istsos4-wizard/src/components/common/FormField.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { AlertTriangle, Info } from 'lucide-react'; import { useWizard } from '../../hooks/useWizard'; -function FormField({ label, children, error, info, fieldName, required = false }) { +function FormField({ label, children, error, fieldName, required = false }) { const { state } = useWizard(); const showError = error && state.validation.touched[fieldName]; @@ -11,12 +11,6 @@ function FormField({ label, children, error, info, fieldName, required = false } {React.cloneElement(children, { className: `${children.props.className} ${ diff --git a/istsos4-wizard/src/components/common/Navigation.jsx b/istsos4-wizard/src/components/common/Navigation.jsx index da729c7..099a7ca 100644 --- a/istsos4-wizard/src/components/common/Navigation.jsx +++ b/istsos4-wizard/src/components/common/Navigation.jsx @@ -1,35 +1,56 @@ -import React from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useWizard } from '../../hooks/useWizard'; +import React, { useState } from "react"; +import { ChevronLeft, ChevronRight, RotateCcw } from "lucide-react"; +import { useWizard } from "../../hooks/useWizard"; +import { useWizardPersistence } from "../../hooks/useWizardPersistence"; const steps = [ - { title: 'Welcome' }, - { title: 'Server Config' }, - { title: 'Database' }, - { title: 'Data Management' }, - { title: 'Sample Data' }, - { title: 'Performance' }, - { title: 'Services' }, - { title: 'Review' }, - { title: 'Complete' } + { title: "Welcome" }, + { title: "Server Config" }, + { title: "Database" }, + { title: "Authorization" }, + { title: "Data Management" }, + { title: "Sample Data" }, + { title: "Performance" }, + { title: "Services" }, + { title: "Review" }, + { title: "Complete" }, ]; function Navigation() { const { state, dispatch } = useWizard(); + const { resetWizard } = useWizardPersistence(); + const [showResetConfirm, setShowResetConfirm] = useState(false); + const [showResetWarning, setShowResetWarning] = useState(false); + const hasErrors = Object.keys(state.validation.errors).length > 0; const hasTouchedErrors = Object.keys(state.validation.errors).some( - field => state.validation.touched[field] + (field) => state.validation.touched[field] ); const canGoNext = state.currentStep < state.totalSteps; const canGoPrev = state.currentStep > 1; const handleNext = () => { - dispatch({ type: 'NEXT_STEP' }); + dispatch({ type: "NEXT_STEP" }); }; const handlePrev = () => { - dispatch({ type: 'PREV_STEP' }); + dispatch({ type: "PREV_STEP" }); + }; + + const handleReset = () => { + if (showResetConfirm) { + resetWizard(); + setShowResetConfirm(false); + setShowResetWarning(false); + } else { + setShowResetWarning(true); + setShowResetConfirm(true); + setTimeout(() => { + setShowResetConfirm(false); + setShowResetWarning(false); + }, 5000); + } }; return ( @@ -38,9 +59,9 @@ function Navigation() { onClick={handlePrev} disabled={!canGoPrev} className={`flex items-center px-4 py-2 rounded-md transition-colors ${ - canGoPrev - ? 'bg-gray-200 hover:bg-gray-300 text-gray-700' - : 'bg-gray-100 text-gray-400 cursor-not-allowed' + canGoPrev + ? "bg-gray-200 hover:bg-gray-300 text-gray-700" + : "bg-gray-100 text-gray-400 cursor-not-allowed" }`} > @@ -58,20 +79,64 @@ function Navigation() { )}
- +
+
+ + + {showResetWarning && showResetConfirm && ( +
+
+ + + +
+

Warning!

+

+ This will permanently delete all your configuration data and + restart the wizard from the beginning. +

+

+ Click again to confirm reset +

+
+
+
+
+ )} +
+ + +
); } -export default Navigation; \ No newline at end of file +export default Navigation; diff --git a/istsos4-wizard/src/components/common/SessionRecovery.jsx b/istsos4-wizard/src/components/common/SessionRecovery.jsx new file mode 100644 index 0000000..70f71aa --- /dev/null +++ b/istsos4-wizard/src/components/common/SessionRecovery.jsx @@ -0,0 +1,43 @@ +import React, { useState, useEffect } from 'react'; +import { AlertCircle, X } from 'lucide-react'; +import { useWizard } from '../../hooks/useWizard'; + +function SessionRecovery() { + const { state } = useWizard(); + const [showNotification, setShowNotification] = useState(false); + + useEffect(() => { + + const savedState = localStorage.getItem('istsos4-wizard-state'); + if (savedState && state.currentStep > 1) { + setShowNotification(true); + + const timer = setTimeout(() => setShowNotification(false), 5000); + return () => clearTimeout(timer); + } + }, []); + + if (!showNotification) return null; + + return ( +
+
+ +
+

Session Recovered

+

+ Your previous progress has been restored. You're on step {state.currentStep} of {state.totalSteps}. +

+
+ +
+
+ ); +} + +export default SessionRecovery; \ No newline at end of file diff --git a/istsos4-wizard/src/components/steps/AuthorizationStep.jsx b/istsos4-wizard/src/components/steps/AuthorizationStep.jsx new file mode 100644 index 0000000..096fa4e --- /dev/null +++ b/istsos4-wizard/src/components/steps/AuthorizationStep.jsx @@ -0,0 +1,285 @@ +import React from "react"; +import { useWizard } from "../../hooks/useWizard"; +import FormField from "../common/FormField"; +import Toggle from "../common/Toggle"; + +function AuthorizationStep() { + const { state, dispatch } = useWizard(); + const { configuration, validation } = state; + + const updateConfig = (field, value) => { + dispatch({ + type: "UPDATE_CONFIG", + payload: { [field]: value }, + }); + }; + + const handleBlur = (field) => { + dispatch({ type: "SET_FIELD_TOUCHED", payload: field }); + }; + + // Function to generate a new secret key + const generateSecretKey = () => { + const chars = "0123456789abcdef"; + let result = ""; + for (let i = 0; i < 64; i++) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + updateConfig("secretKey", result); + }; + + // Password strength indicator + const getPasswordStrength = (password) => { + if (!password) return { strength: 0, label: "" }; + + let strength = 0; + if (password.length >= 8) strength++; + if (password.length >= 12) strength++; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; + if (/\d/.test(password)) strength++; + if (/[^a-zA-Z0-9]/.test(password)) strength++; + + const labels = ["", "Weak", "Fair", "Good", "Strong", "Very Strong"]; + return { strength, label: labels[strength] }; + }; + + const passwordStrength = getPasswordStrength( + configuration.istsosAdminPassword + ); + + return ( +
+

+ Authorization Configuration +

+

+ Configure authentication and authorization settings for your istSOS4 + instance. +

+ +
+
+ {/* Authorization Toggle */} + + + updateConfig("authorization", e.target.checked ? 1 : 0) + } + label={configuration.authorization === 1 ? "Enabled" : "Disabled"} + /> + + + + {/* Admin Username */} + + updateConfig("istsosAdmin", e.target.value)} + onBlur={() => handleBlur("istsosAdmin")} + placeholder="admin" + /> + + {/* Admin Password */} + +
+
+ + updateConfig("istsosAdminPassword", e.target.value) + } + onBlur={() => handleBlur("istsosAdminPassword")} + placeholder="Enter administrator password" + /> +
+ + {/* Password strength indicator */} + {configuration.istsosAdminPassword && ( +
+
+ Password strength: + + {passwordStrength.label} + +
+
+
+
+
+ )} +
+ +
+ +
+ {/* Anonymous Viewer */} + + + updateConfig("anonymousViewer", e.target.checked ? 1 : 0) + } + label={ + configuration.anonymousViewer === 1 ? "Enabled" : "Disabled" + } + /> + + + {/* Access Token Expire Minutes */} + + { + const value = e.target.value; + updateConfig( + "accessTokenExpireMinutes", + value === "" ? "" : parseInt(value) + ); + }} + onBlur={() => handleBlur("accessTokenExpireMinutes")} + placeholder="5" + /> + + {/* Algorithm */} + + + +
+
+ + {/* Secret Key */} +
+ +
+
+ updateConfig("secretKey", e.target.value)} + onBlur={() => handleBlur("secretKey")} + placeholder="09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" + /> + +
+

+ Use 'openssl rand -hex 32' to generate a secure key, or click + "Generate New" +

+
+
+
+ +
+
+
+ + + +
+
+

+ Security Best Practices +

+
+
    +
  • Use a strong, randomly generated secret key
  • +
  • + Set appropriate token expiration times for your use case +
  • +
  • Use a strong password for the administrator account
  • +
  • + Consider disabling anonymous viewer in production environments +
  • +
+
+
+
+
+
+ ); +} + +export default AuthorizationStep; diff --git a/istsos4-wizard/src/components/steps/BasicServerStep.jsx b/istsos4-wizard/src/components/steps/BasicServerStep.jsx index 0902cc7..c088a3d 100644 --- a/istsos4-wizard/src/components/steps/BasicServerStep.jsx +++ b/istsos4-wizard/src/components/steps/BasicServerStep.jsx @@ -25,7 +25,6 @@ function BasicServerStep() {
@@ -23,4 +24,5 @@ function CompletionStep() {
); } + export default CompletionStep; \ No newline at end of file diff --git a/istsos4-wizard/src/components/steps/DataManagementStep.jsx b/istsos4-wizard/src/components/steps/DataManagementStep.jsx index 2b712e1..7d2ce4a 100644 --- a/istsos4-wizard/src/components/steps/DataManagementStep.jsx +++ b/istsos4-wizard/src/components/steps/DataManagementStep.jsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { useWizard } from '../../hooks/useWizard'; -import Toggle from '../common/Toggle'; +import React from "react"; +import { useWizard } from "../../hooks/useWizard"; +import Toggle from "../common/Toggle"; function DataManagementStep() { const { state, dispatch } = useWizard(); @@ -8,34 +8,43 @@ function DataManagementStep() { const updateConfig = (field, value) => { dispatch({ - type: 'UPDATE_CONFIG', - payload: { [field]: value } + type: "UPDATE_CONFIG", + payload: { [field]: value }, }); }; return (
-

Data Management Configuration

- +

+ Data Management Configuration +

+
-

Data Initialization

+

+ Data Initialization +

updateConfig('dummyData', e.target.checked ? 1 : 0)} + onChange={(e) => + updateConfig("dummyData", e.target.checked ? 1 : 0) + } label="Generate dummy data for testing" />

- Automatically populate the database with sample sensor data for testing + Automatically populate the database with sample sensor data for + testing

- + updateConfig('clearData', e.target.checked ? 1 : 0)} + onChange={(e) => + updateConfig("clearData", e.target.checked ? 1 : 0) + } label="Clear existing data on startup" /> - + {configuration.clearData === 1 && (

@@ -54,21 +63,47 @@ function DataManagementStep() {

updateConfig('versioning', e.target.checked)} + onChange={(e) => + updateConfig("versioning", e.target.checked ? 1 : 0) + } label="Enable data versioning" />

Keep historical versions of all data changes

- + {configuration.versioning === 1 && ( +
+

+ ⚠ Warning: This option cannot be modified once the setup is + complete! +

+

+ Enabling this will alter your system's data handling behavior + permanently. +

+
+ )} updateConfig('duplicates', e.target.checked)} + onChange={(e) => + updateConfig("duplicates", e.target.checked ? 1 : 0) + } label="Allow duplicate entries" />

Permit multiple identical observations at the same timestamp

+ {configuration.duplicates === 1 && ( +
+

+ ⚠ Warning: This option cannot be modified once the setup is + complete! +

+

+ Enabling this will permanently change how your system validates and stores incoming data. +

+
+ )}
@@ -76,4 +111,4 @@ function DataManagementStep() { ); } -export default DataManagementStep; \ No newline at end of file +export default DataManagementStep; diff --git a/istsos4-wizard/src/components/steps/DatabaseStep.jsx b/istsos4-wizard/src/components/steps/DatabaseStep.jsx index d085571..caf4671 100644 --- a/istsos4-wizard/src/components/steps/DatabaseStep.jsx +++ b/istsos4-wizard/src/components/steps/DatabaseStep.jsx @@ -1,37 +1,35 @@ -import React, { useState } from 'react'; -import { Eye, EyeOff } from 'lucide-react'; -import { useWizard } from '../../hooks/useWizard'; -import FormField from '../common/FormField'; +import React, { useState } from "react"; +import { useWizard } from "../../hooks/useWizard"; +import FormField from "../common/FormField"; function DatabaseStep() { const { state, dispatch } = useWizard(); const { configuration, validation } = state; - const [showPassword, setShowPassword] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const updateConfig = (field, value) => { dispatch({ - type: 'UPDATE_CONFIG', - payload: { [field]: value } + type: "UPDATE_CONFIG", + payload: { [field]: value }, }); }; const handleBlur = (field) => { - dispatch({ type: 'SET_FIELD_TOUCHED', payload: field }); + dispatch({ type: "SET_FIELD_TOUCHED", payload: field }); }; // Password strength indicator const getPasswordStrength = (password) => { - if (!password) return { strength: 0, label: '' }; - + if (!password) return { strength: 0, label: "" }; + let strength = 0; if (password.length >= 8) strength++; if (password.length >= 12) strength++; if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; if (/\d/.test(password)) strength++; if (/[^a-zA-Z0-9]/.test(password)) strength++; - - const labels = ['', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong']; + + const labels = ["", "Weak", "Fair", "Good", "Strong", "Very Strong"]; return { strength, label: labels[strength] }; }; @@ -39,44 +37,44 @@ function DatabaseStep() { return (
-

Database Configuration

- +

+ Database Configuration +

+
- updateConfig('postgresDb', e.target.value)} - onBlur={() => handleBlur('postgresDb')} + onChange={(e) => updateConfig("postgresDb", e.target.value)} + onBlur={() => handleBlur("postgresDb")} placeholder="istsos" /> - updateConfig('postgresUser', e.target.value)} - onBlur={() => handleBlur('postgresUser')} + onChange={(e) => updateConfig("postgresUser", e.target.value)} + onBlur={() => handleBlur("postgresUser")} placeholder="postgres" /> -
updateConfig('postgresPassword', e.target.value)} - onBlur={() => handleBlur('postgresPassword')} + onChange={(e) => + updateConfig("postgresPassword", e.target.value) + } + onBlur={() => handleBlur("postgresPassword")} placeholder="Enter secure password" /> -
- + {/* Password strength indicator */} {configuration.postgresPassword && (
Password strength: - + {passwordStrength.label}
-
@@ -136,14 +137,13 @@ function DatabaseStep() { className="text-blue-600 hover:text-blue-800 font-medium" onClick={() => setShowAdvanced(!showAdvanced)} > - {showAdvanced ? 'Hide' : 'Show'} Advanced Settings + {showAdvanced ? "Hide" : "Show"} Advanced Settings {showAdvanced && ( -
- + @@ -153,14 +153,19 @@ function DatabaseStep() { max="50" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value={configuration.pgPoolSize} - onChange={(e) => updateConfig('pgPoolSize', parseInt(e.target.value) || 0)} - onBlur={() => handleBlur('pgPoolSize')} + onChange={(e) => { + const value = e.target.value; + updateConfig( + "pgPoolSize", + value === "" ? "" : parseInt(value) + ); + }} + onBlur={() => handleBlur("pgPoolSize")} /> - @@ -170,14 +175,19 @@ function DatabaseStep() { max="20" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value={configuration.pgMaxOverflow} - onChange={(e) => updateConfig('pgMaxOverflow', parseInt(e.target.value) || 0)} - onBlur={() => handleBlur('pgMaxOverflow')} + onChange={(e) => { + const value = e.target.value; + updateConfig( + "pgMaxOverflow", + value === "" ? "" : parseInt(value) + ); + }} + onBlur={() => handleBlur("pgMaxOverflow")} /> - @@ -187,8 +197,11 @@ function DatabaseStep() { max="120" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value={configuration.pgPoolTimeout} - onChange={(e) => updateConfig('pgPoolTimeout', parseInt(e.target.value) || 0)} - onBlur={() => handleBlur('pgPoolTimeout')} + onChange={(e) => { + const value = e.target.value; + updateConfig('pgPoolTimeout', value === '' ? '' : parseInt(value));} + } + onBlur={() => handleBlur("pgPoolTimeout")} />
@@ -198,4 +211,4 @@ function DatabaseStep() { ); } -export default DatabaseStep; \ No newline at end of file +export default DatabaseStep; diff --git a/istsos4-wizard/src/components/steps/PerformanceStep.jsx b/istsos4-wizard/src/components/steps/PerformanceStep.jsx index c59e8d2..591f9a1 100644 --- a/istsos4-wizard/src/components/steps/PerformanceStep.jsx +++ b/istsos4-wizard/src/components/steps/PerformanceStep.jsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { useWizard } from '../../hooks/useWizard'; -import FormField from '../common/FormField'; +import React from "react"; +import { useWizard } from "../../hooks/useWizard"; +import FormField from "../common/FormField"; +import Toggle from "../common/Toggle"; function PerformanceStep() { const { state, dispatch } = useWizard(); @@ -8,53 +9,92 @@ function PerformanceStep() { const updateConfig = (field, value) => { dispatch({ - type: 'UPDATE_CONFIG', - payload: { [field]: value } + type: "UPDATE_CONFIG", + payload: { [field]: value }, }); }; const handleBlur = (field) => { - dispatch({ type: 'SET_FIELD_TOUCHED', payload: field }); + dispatch({ type: "SET_FIELD_TOUCHED", payload: field }); }; return (
-

Performance & Advanced Settings

-

Configure performance optimization and advanced database settings

- +

+ Performance & Advanced Settings +

+

+ Configure performance optimization and advanced database settings +

+
+
+
+ updateConfig("redis", e.target.checked ? 1 : 0)} + label="Enable Redis Caching" + /> +

+ Redis improves performance by caching frequently accessed data and + API responses +

+ + {configuration.redis === 1 && ( +
+
+

+ Note: Ensure Redis is included in your + Docker Compose configuration +

+
+
    +
  • • Default connection: redis://redis:6379
  • +
  • • Recommended memory: 512MB minimum
  • +
  • • Cache TTL: 1 hour (configurable)
  • +
+
+ )} +
+
+
+
-
{[ - { - value: 'FULL', - label: 'Full Count', - desc: 'Always count all results (accurate but slow for large datasets)', - recommended: configuration.countEstimateThreshold < 10000 + { + value: "FULL", + label: "Full Count", + desc: "Always count all results (accurate but slow for large datasets)", + recommended: configuration.countEstimateThreshold < 10000, }, - { - value: 'LIMIT_ESTIMATE', - label: 'Limit + Estimate', - desc: 'Count up to threshold, then estimate (balanced approach)', - recommended: configuration.countEstimateThreshold >= 10000 && configuration.countEstimateThreshold <= 50000 + { + value: "LIMIT_ESTIMATE", + label: "Limit + Estimate", + desc: "Count up to threshold, then estimate (balanced approach)", + recommended: + configuration.countEstimateThreshold >= 10000 && + configuration.countEstimateThreshold <= 50000, + }, + { + value: "ESTIMATE_LIMIT", + label: "Estimate + Limit", + desc: "Estimate first, count if below threshold (fast but less accurate)", + recommended: configuration.countEstimateThreshold > 50000, }, - { - value: 'ESTIMATE_LIMIT', - label: 'Estimate + Limit', - desc: 'Estimate first, count if below threshold (fast but less accurate)', - recommended: configuration.countEstimateThreshold > 50000 - } ].map((mode) => ( -
-

Data Generation Summary

+

+ Data Generation Summary +

- Total sensors: {configuration.nThings} + Total sensors:{" "} + {configuration.nThings}

- Properties per sensor: {configuration.nObservedProperties} + Properties per sensor:{" "} + {configuration.nObservedProperties}

- Estimated data points: {calculateDataPoints().toLocaleString()} + Estimated data points:{" "} + {calculateDataPoints().toLocaleString()}

{calculateDataPoints() > 1000000 && ( @@ -167,8 +276,10 @@ function SampleDataStep() {

)}
+ +
); } -export default SampleDataStep; \ No newline at end of file +export default SampleDataStep; diff --git a/istsos4-wizard/src/components/steps/ServicesStep.jsx b/istsos4-wizard/src/components/steps/ServicesStep.jsx index 009be5a..9d3b965 100644 --- a/istsos4-wizard/src/components/steps/ServicesStep.jsx +++ b/istsos4-wizard/src/components/steps/ServicesStep.jsx @@ -1,105 +1,164 @@ -import React from 'react'; -import { useWizard } from '../../hooks/useWizard'; -import FormField from '../common/FormField'; -import Toggle from '../common/Toggle'; +import React from "react"; +import { useWizard } from "../../hooks/useWizard"; +import FormField from "../common/FormField"; +import Toggle from "../common/Toggle"; function ServicesStep() { const { state, dispatch } = useWizard(); - const { configuration } = state; + const { configuration, validation } = state; const updateConfig = (field, value) => { dispatch({ - type: 'UPDATE_CONFIG', - payload: { [field]: value } + type: "UPDATE_CONFIG", + payload: { [field]: value }, }); }; - const epsgOptions = [ - { value: 4326, label: 'EPSG:4326 (WGS 84)', desc: 'Global GPS coordinates' }, - { value: 3857, label: 'EPSG:3857 (Web Mercator)', desc: 'Web mapping standard' }, - { value: 2154, label: 'EPSG:2154 (RGF93 / Lambert-93)', desc: 'France' }, - { value: 25832, label: 'EPSG:25832 (ETRS89 / UTM zone 32N)', desc: 'Central Europe' }, - { value: 32633, label: 'EPSG:32633 (WGS 84 / UTM zone 33N)', desc: 'Eastern Europe' }, - { value: 4269, label: 'EPSG:4269 (NAD83)', desc: 'North America' } - ]; + const handleBlur = (field) => { + dispatch({ type: "SET_FIELD_TOUCHED", payload: field }); + }; return (

Additional Services

-

Configure optional services and coordinate systems

- +

+ Configure optional services and coordinate systems +

+
-
-
-
- updateConfig('redis', e.target.checked ? 1 : 0)} - label="Enable Redis Caching" - /> -

- Redis improves performance by caching frequently accessed data and API responses -

- - {configuration.redis === 1 && ( -
-
-

- Note: Ensure Redis is included in your Docker Compose configuration -

-
-
    -
  • • Default connection: redis://redis:6379
  • -
  • • Recommended memory: 512MB minimum
  • -
  • • Cache TTL: 1 hour (configurable)
  • -
-
- )} -
-
-
+ - -
- - -
- {configuration.epsg === 4326 && ( -

- ✓ WGS 84 is recommended for global applications and GPS data -

- )} - {configuration.epsg === 3857 && ( -

- ℹ Web Mercator is ideal for web mapping applications -

- )} +
+ {/* EPSG Input */} +
+ { + const value = e.target.value; + updateConfig("epsg", value === "" ? "" : parseInt(value)); + }} + onBlur={() => handleBlur("epsg")} + />
+ + {/* EPSG code validation */} + {configuration.epsg && ( +
+

+ Selected EPSG Code: {configuration.epsg} +

+

+ Make sure this code is valid for your geographic area and data + requirements. +

+
+ )} + + {/* Common EPSG codes hints */} +
+ Common EPSG Codes: +
+
+

Global:

+
    +
  • + • 4326 - WGS 84 (GPS coordinates) +
  • +
  • + • 3857 - Web Mercator (Google Maps) +
  • +
  • + • 4269 - NAD83 (North America) +
  • +
  • + • 4258 - ETRS89 (Europe) +
  • +
+
+
+

Regional:

+
    +
  • + • 2154 - Lambert-93 (France) +
  • +
  • + • 27700 - British National Grid (UK) +
  • +
  • + • 32633 - UTM Zone 33N (Europe) +
  • +
  • + • 3035 - ETRS89 LAEA (Europe) +
  • +
+
+
+
+ + {/* How to find EPSG codes */} +
+ How to find your EPSG code: +
    +
  • + • Visit{" "} + + epsg.io + {" "} + to search by location or name +
  • +
  • + • Use{" "} + + spatialreference.org + {" "} + for detailed information +
  • +
  • • Check your existing GIS data properties
  • +
  • + • For GPS data, use 4326 (WGS 84) +
  • +
  • + • For web mapping, use 3857 (Web Mercator) +
  • +
+
-

Service Architecture

+

+ Service Architecture +

-
-
- Redis Cache {configuration.redis === 1 ? '(Enabled)' : '(Disabled)'} -
+ {/*
+
+ + Redis Cache{" "} + {configuration.redis === 1 ? "(Enabled)" : "(Disabled)"} + +
*/}
PostgreSQL Database (Required) @@ -115,4 +174,4 @@ function ServicesStep() { ); } -export default ServicesStep; \ No newline at end of file +export default ServicesStep; diff --git a/istsos4-wizard/src/components/wizard/StepRenderer.jsx b/istsos4-wizard/src/components/wizard/StepRenderer.jsx index b9be377..d18213e 100644 --- a/istsos4-wizard/src/components/wizard/StepRenderer.jsx +++ b/istsos4-wizard/src/components/wizard/StepRenderer.jsx @@ -11,6 +11,7 @@ import PerformanceStep from '../steps/PerformanceStep'; import ServicesStep from '../steps/ServicesStep'; import ReviewStep from '../steps/ReviewStep'; import CompletionStep from '../steps/CompletionStep'; +import AuthorizationStep from '../steps/AuthorizationStep'; function StepRenderer() { const { state } = useWizard(); @@ -19,6 +20,7 @@ function StepRenderer() { WelcomeStep, BasicServerStep, DatabaseStep, + AuthorizationStep, DataManagementStep, SampleDataStep, PerformanceStep, diff --git a/istsos4-wizard/src/components/wizard/WizardProvider.jsx b/istsos4-wizard/src/components/wizard/WizardProvider.jsx index 6ec1651..14cc420 100644 --- a/istsos4-wizard/src/components/wizard/WizardProvider.jsx +++ b/istsos4-wizard/src/components/wizard/WizardProvider.jsx @@ -1,12 +1,100 @@ +import React, { useReducer, useEffect, useRef, useState } from "react"; +import { WizardContext } from "../../context/WizardContext"; +import { wizardReducer } from "../../reducers/wizardReducer"; +import { initialState } from "../../utils/constants"; -import React, { useReducer } from 'react'; -import { WizardContext } from '../../context/WizardContext'; -import { wizardReducer } from '../../reducers/wizardReducer'; -import { initialState } from '../../utils/constants'; +const STORAGE_KEY = "istsos4-wizard-state"; +const SAVE_DELAY = 1000; function WizardProvider({ children }) { - const [state, dispatch] = useReducer(wizardReducer, initialState); - + const [lastSaved, setLastSaved] = useState(null); + const saveTimeoutRef = useRef(null); + + const loadInitialState = () => { + try { + const savedState = localStorage.getItem(STORAGE_KEY); + if (savedState) { + const parsedState = JSON.parse(savedState); + + const savedConfig = { ...parsedState.configuration }; + delete savedConfig.postgresPassword; + delete savedConfig.istsosAdminPassword; + delete savedConfig.secretKey; + + return { + ...initialState, + ...parsedState, + validation: { + ...initialState.validation, + ...parsedState.validation, + }, + configuration: { + ...initialState.configuration, + ...savedConfig, + }, + }; + } + } catch (error) { + console.error("Error loading saved state:", error); + } + return initialState; + }; + + const [state, dispatch] = useReducer(wizardReducer, loadInitialState()); + + // Debounced save to localStorage + useEffect(() => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + + saveTimeoutRef.current = setTimeout(() => { + try { + const stateToSave = { + ...state, + configuration: { + ...state.configuration, + }, + savedAt: Date.now(), + }; + + // Remove sensitive fields before saving + delete stateToSave.configuration.postgresPassword; + delete stateToSave.configuration.istsosAdminPassword; + delete stateToSave.configuration.secretKey; + + localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); + setLastSaved(Date.now()); + } catch (error) { + console.error("Error saving state:", error); + } + }, SAVE_DELAY); + + return () => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + }; + }, [state]); + + // Listen for storage changes from other tabs + useEffect(() => { + const handleStorageChange = (e) => { + if (e.key === STORAGE_KEY && e.newValue && e.newValue !== e.oldValue) { + if ( + window.confirm( + "The wizard has been updated in another tab. Do you want to load those changes?" + ) + ) { + window.location.reload(); + } + } + }; + + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); + }, []); + return ( {children} @@ -14,4 +102,4 @@ function WizardProvider({ children }) { ); } -export default WizardProvider; \ No newline at end of file +export default WizardProvider; diff --git a/istsos4-wizard/src/hooks/useWizardPersistence.js b/istsos4-wizard/src/hooks/useWizardPersistence.js new file mode 100644 index 0000000..e65fbc4 --- /dev/null +++ b/istsos4-wizard/src/hooks/useWizardPersistence.js @@ -0,0 +1,18 @@ +import { useWizard } from './useWizard'; + +export function useWizardPersistence() { + const { state, dispatch } = useWizard(); + + const resetWizard = () => { + dispatch({ type: 'RESET_WIZARD' }); + }; + + const clearPersistedData = () => { + localStorage.removeItem('istsos4-wizard-state'); + }; + + return { + resetWizard, + clearPersistedData, + }; +} diff --git a/istsos4-wizard/src/index.css b/istsos4-wizard/src/index.css index a461c50..b685ac1 100644 --- a/istsos4-wizard/src/index.css +++ b/istsos4-wizard/src/index.css @@ -1 +1,15 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in { + animation: fade-in 0.3s ease-out; +} \ No newline at end of file diff --git a/istsos4-wizard/src/reducers/wizardReducer.js b/istsos4-wizard/src/reducers/wizardReducer.js index 1a4326a..0839af3 100644 --- a/istsos4-wizard/src/reducers/wizardReducer.js +++ b/istsos4-wizard/src/reducers/wizardReducer.js @@ -1,25 +1,27 @@ import { validateField, validateStep } from '../utils/fieldValidations'; +import { initialState } from '../utils/constants'; + export function wizardReducer(state, action) { switch (action.type) { case 'SET_STEP': return { ...state, currentStep: action.payload }; - + case 'UPDATE_CONFIG': const newConfig = { ...state.configuration, ...action.payload }; const fieldName = Object.keys(action.payload)[0]; const fieldValue = action.payload[fieldName]; - - const fieldError = validateField(fieldName, fieldValue); - - + + const fieldError = validateField(fieldName, fieldValue); + + const newErrors = { ...state.validation.errors }; if (fieldError) { newErrors[fieldName] = fieldError; } else { delete newErrors[fieldName]; } - + return { ...state, configuration: newConfig, @@ -29,7 +31,7 @@ export function wizardReducer(state, action) { touched: { ...state.validation.touched, [fieldName]: true } } }; - + case 'SET_FIELD_TOUCHED': return { ...state, @@ -38,7 +40,7 @@ export function wizardReducer(state, action) { touched: { ...state.validation.touched, [action.payload]: true } } }; - + case 'TOUCH_MULTIPLE_FIELDS': const newTouched = { ...state.validation.touched }; action.payload.forEach(field => { @@ -51,7 +53,7 @@ export function wizardReducer(state, action) { touched: newTouched } }; - + case 'VALIDATE_CURRENT_STEP': const stepErrors = validateStep(state.currentStep, state.configuration); return { @@ -62,19 +64,19 @@ export function wizardReducer(state, action) { isValid: Object.keys(stepErrors).length === 0 } }; - + case 'NEXT_STEP': - + const currentStepErrors = validateStep(state.currentStep, state.configuration); - + if (Object.keys(currentStepErrors).length > 0) { - + const errorFields = Object.keys(currentStepErrors); const touchedFields = { ...state.validation.touched }; errorFields.forEach(field => { touchedFields[field] = true; }); - + return { ...state, validation: { @@ -84,19 +86,23 @@ export function wizardReducer(state, action) { } }; } - + return { ...state, currentStep: Math.min(state.currentStep + 1, state.totalSteps), validation: { ...state.validation, isValid: true } }; - + case 'PREV_STEP': return { ...state, currentStep: Math.max(state.currentStep - 1, 1) }; - + + case 'RESET_WIZARD': + localStorage.removeItem('istsos4-wizard-state'); + return initialState; + default: return state; } diff --git a/istsos4-wizard/src/utils/constants.js b/istsos4-wizard/src/utils/constants.js index ab91802..d63aadb 100644 --- a/istsos4-wizard/src/utils/constants.js +++ b/istsos4-wizard/src/utils/constants.js @@ -1,57 +1,7 @@ -// // src/utils/constants.js -// export const initialState = { -// currentStep: 1, -// totalSteps: 9, -// configuration: { -// // Basic Server Configuration -// hostname: 'http://localhost:8018', -// subpath: '/istsos4', -// version: '/v1.1', -// debug: 0, - -// // Database Configuration -// postgresDb: 'istsos', -// postgresUser: 'admin', -// postgresPassword: 'admin', -// pgMaxOverflow: 0, -// pgPoolSize: 10, -// pgPoolTimeout: 30, - -// // Data Management -// dummyData: 0, -// clearData: 0, -// versioning: false, -// duplicates: false, - -// // Sample Data (conditional) -// nThings: 3, -// nObservedProperties: 2, -// interval: 'P1Y', -// frequency: 'PT5M', -// startDatetime: '2020-01-01T12:00:00.000+01:00', - -// // Performance Settings -// countMode: 'FULL', -// countEstimateThreshold: 10000, -// topValue: 100, -// partitionChunk: 10000, -// chunkInterval: 'P1Y', - -// // Additional Services -// redis: 1, -// epsg: 4326 -// }, -// validation: { -// errors: {}, -// touched: {}, -// isValid: true -// } -// }; - export const initialState = { // Wizard State currentStep: 1, - totalSteps: 9, + totalSteps: 10, validation: { errors: {}, touched: {}, @@ -73,12 +23,21 @@ export const initialState = { pgMaxOverflow: 0, pgPoolSize: 10, pgPoolTimeout: 30, + + // Authentication Configuration + istsosAdmin: 'admin', + istsosAdminPassword: '', + authorization: 0, + secretKey: '09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7', + algorithm: 'HS256', + accessTokenExpireMinutes: '5', + anonymousViewer: 0, // Data Management dummyData: 0, clearData: 0, - versioning: false, - duplicates: false, + versioning: 0, + duplicates: 0, // Sample Data nThings: 3, @@ -93,9 +52,9 @@ export const initialState = { topValue: 100, partitionChunk: 10000, chunkInterval: 'P1Y', + redis: 0, // Additional Services - redis: 0, epsg: 4326 } }; \ No newline at end of file diff --git a/istsos4-wizard/src/utils/fieldValidations.js b/istsos4-wizard/src/utils/fieldValidations.js index c08d589..ecc2317 100644 --- a/istsos4-wizard/src/utils/fieldValidations.js +++ b/istsos4-wizard/src/utils/fieldValidations.js @@ -10,7 +10,7 @@ export const fieldValidations = { ValidationRules.required, ValidationRules.pattern(/^\/[a-zA-Z0-9_-]+$/, 'Must start with / and contain only letters, numbers, hyphens, and underscores') ], - + // Database Configuration postgresDb: [ ValidationRules.required, @@ -32,6 +32,7 @@ export const fieldValidations = { ValidationRules.maxValue(50) ], pgMaxOverflow: [ + ValidationRules.required, ValidationRules.minValue(0), ValidationRules.maxValue(20) ], @@ -39,17 +40,46 @@ export const fieldValidations = { ValidationRules.minValue(10), ValidationRules.maxValue(120) ], - + + // Authentication Configuration + istsosAdmin: [ + ValidationRules.required, + ValidationRules.alphanumeric, + ValidationRules.minLength(1), + ValidationRules.maxLength(63) + ], + istsosAdminPassword: [ + ValidationRules.required, + ValidationRules.password + ], + secretKey: [ + ValidationRules.required, + ValidationRules.minLength(32), + ValidationRules.maxLength(64) + ], + accessTokenExpireMinutes: [ + ValidationRules.required, + ValidationRules.minValue(1), + ValidationRules.maxValue(1440) // 1 day in minutes + ], + + // Sample Data Configuration nThings: [ + ValidationRules.required, ValidationRules.minValue(1), ValidationRules.maxValue(100) ], nObservedProperties: [ + ValidationRules.required, ValidationRules.minValue(1), ValidationRules.maxValue(10) ], - + partitionChunk: [ + ValidationRules.minValue(1000), + ValidationRules.maxValue(50000) + ], + // Performance Settings countEstimateThreshold: [ ValidationRules.minValue(1000), @@ -59,22 +89,27 @@ export const fieldValidations = { ValidationRules.minValue(10), ValidationRules.maxValue(1000) ], - partitionChunk: [ + + // Additional Services + epsg: [ + ValidationRules.required, ValidationRules.minValue(1000), - ValidationRules.maxValue(50000) - ] + ValidationRules.maxValue(999999) + ], + + }; // Validate a single field export const validateField = (fieldName, value) => { const validators = fieldValidations[fieldName]; if (!validators) return null; - + for (const validator of validators) { const error = validator(value); if (error) return error; } - + return null; }; @@ -82,11 +117,12 @@ export const validateField = (fieldName, value) => { export const validateStep = (stepNumber, configuration) => { const errors = {}; let fieldsToValidate = []; - + switch (stepNumber) { case 2: // Basic Server Configuration fieldsToValidate = ['hostname', 'subpath']; break; + case 3: // Database Configuration fieldsToValidate = ['postgresDb', 'postgresUser', 'postgresPassword']; // Include advanced fields if shown @@ -94,36 +130,46 @@ export const validateStep = (stepNumber, configuration) => { fieldsToValidate.push('pgPoolSize', 'pgMaxOverflow', 'pgPoolTimeout'); } break; - case 5: // Sample Data Configuration + + case 4: // Authentication Configuration + fieldsToValidate = ['istsosAdmin', 'istsosAdminPassword', 'secretKey', 'accessTokenExpireMinutes' ]; + break; + + case 6: // Sample Data Configuration if (configuration.dummyData === 1) { - fieldsToValidate = ['nThings', 'nObservedProperties']; + fieldsToValidate = ['nThings', 'nObservedProperties', 'partitionChunk']; } break; - case 6: // Performance Settings - fieldsToValidate = ['countEstimateThreshold', 'topValue', 'partitionChunk']; + + case 7: // Performance Settings + fieldsToValidate = ['countEstimateThreshold', 'topValue']; + break; + + case 8: // Additional Services + fieldsToValidate = ['epsg']; break; } - + fieldsToValidate.forEach(field => { const error = validateField(field, configuration[field]); if (error) { errors[field] = error; } }); - + return errors; }; // Validate entire configuration export const validateConfiguration = (configuration) => { const errors = {}; - + Object.keys(fieldValidations).forEach(field => { const error = validateField(field, configuration[field]); if (error) { errors[field] = error; } }); - + return errors; }; \ No newline at end of file