Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Netmanager: The Analytics Page and the export-data page #2386

Merged
merged 28 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 5 additions & 33 deletions netmanager-app/app/(authenticated)/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,8 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import React from 'react'
import Analytics from '@/components/Analytics'

export default function Analytics() {
export default function AnalyticsPage() {
return (
<div className="p-6">
<h1 className="text-3xl font-semibold text-foreground mb-6">Dashboard</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle>Total Devices</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">156</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Active Sites</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">42</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Data Points Collected</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">1.2M</p>
</CardContent>
</Card>
</div>
</div>
);
<Analytics />
)
}
34 changes: 34 additions & 0 deletions netmanager-app/app/(authenticated)/data-export/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'

import { useState } from 'react'
import { Card } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import ExportForm from '@/components/export-data/ExportForm'
import { ExportType } from '@/app/types/export'

export default function ExportData() {
const [activeTab, setActiveTab] = useState<ExportType>('sites')

return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold text-center mb-4">Export Data</h1>
<p className="mb-4 text-center text-gray-600">
Customize the data you want to download. We recommend downloading data for shorter time
periods like a week or a month to avoid timeouts.
</p>
<Card>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as ExportType)}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="sites">Sites</TabsTrigger>
<TabsTrigger value="devices">Devices</TabsTrigger>
<TabsTrigger value="airqlouds">AirQlouds</TabsTrigger>
</TabsList>
<TabsContent value={activeTab}>
<ExportForm exportType={activeTab} />
</TabsContent>
</Tabs>
</Card>
</div>
)
}

8 changes: 8 additions & 0 deletions netmanager-app/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-6: 210 76% 55%;
--chart-7: 0 76% 60%;
--chart-8: 120 58% 45%;
--chart-9: 60 70% 50%;
}

.dark {
Expand Down Expand Up @@ -56,6 +60,10 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--chart-6: 200 75% 60%;
--chart-7: 360 75% 55%;
--chart-8: 150 65% 50%;
--chart-9: 50 80% 60%;
}
}

Expand Down
74 changes: 74 additions & 0 deletions netmanager-app/app/types/cohorts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export interface Cohort {
_id: string;
visibility: boolean;
cohort_tags: string[];
cohort_codes: string[];
name: string;
network: string;
groups: string[];
numberOfDevices: number;
devices: Device[];
}

interface Grid {
_id: string;
visibility: boolean;
name: string;
admin_level: string;
network: string;
long_name: string;
createdAt: string;
sites: Site[];
}
Comment on lines +13 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Export the Grid interface.

The Grid interface should be exported as it's likely to be used in other files.

- interface Grid {
+ export interface Grid {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface Grid {
_id: string;
visibility: boolean;
name: string;
admin_level: string;
network: string;
long_name: string;
createdAt: string;
sites: Site[];
}
export interface Grid {
_id: string;
visibility: boolean;
name: string;
admin_level: string;
network: string;
long_name: string;
createdAt: string;
sites: Site[];
}


interface Site {
_id: string;
isOnline: boolean;
formatted_name: string;
location_name: string;
search_name: string;
city: string;
district: string;
county: string;
region: string;
country: string;
latitude: number;
longitude: number;
name: string;
approximate_latitude: number;
approximate_longitude: number;
generated_name: string;
data_provider: string;
description: string;
site_category: SiteCategory;
groups: string[];
grids: Grid[];
devices: Device[];
airqlouds: unknown[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Specify the type for airqlouds array.

Using unknown[] is not type-safe. Consider creating a proper type for airqlouds.

- airqlouds: unknown[];
+ airqlouds: Array<{ _id: string; name: string; /* other properties */ }>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
airqlouds: unknown[];
airqlouds: Array<{ _id: string; name: string; /* other properties */ }>;

createdAt: string;
updatedAt?: string;
}

interface SiteCategory {
tags: string[];
area_name: string;
category: string;
highway: string;
landuse: string;
latitude: number;
longitude: number;
natural: string;
search_radius: number;
waterway: string;
}

interface Device {
_id: string;
name: string;
network: string;
groups: string[];
authRequired: boolean;
serial_number: string;
api_code: string;
long_name: string;
}
17 changes: 17 additions & 0 deletions netmanager-app/app/types/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Option } from "@/components/ui/multi-select"

export type ExportType = 'sites' | 'devices' | 'airqlouds' | 'regions'

export interface FormData {
startDateTime: string;
endDateTime: string;
sites?: Option[];
devices?: Option[];
cities?: Option[];
regions?: Option[];
pollutants?: Option[];
frequency: string
fileType: string
outputFormat: string
dataType: string
}
60 changes: 60 additions & 0 deletions netmanager-app/app/types/grids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export interface Grid {
_id: string;
visibility: boolean;
name: string;
admin_level: string;
network: string;
long_name: string;
createdAt: string;
sites: Site[];
}
Comment on lines +1 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider breaking potential circular dependency between Grid and Site interfaces

The Grid interface contains sites: Site[] while the Site interface contains grids: Grid[]. This circular reference could cause issues with TypeScript's type system and make the code harder to maintain.

Consider:

  1. Breaking this circular dependency by creating a minimal GridReference interface
  2. Using it in the Site interface instead of the full Grid interface
+interface GridReference {
+    _id: string;
+    name: string;
+    long_name: string;
+}

 interface Site {
     // ... other fields ...
-    grids: Grid[];
+    grids: GridReference[];
 }

Also applies to: 12-38


interface Site {
_id: string;
isOnline: boolean;
formatted_name: string;
location_name: string;
search_name: string;
city: string;
district: string;
county: string;
region: string;
country: string;
latitude: number;
longitude: number;
name: string;
approximate_latitude: number;
approximate_longitude: number;
generated_name: string;
data_provider: string;
description: string;
site_category: SiteCategory;
groups: string[];
grids: Grid[];
devices: Device[];
airqlouds: unknown[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety for airqlouds array

The airqlouds property uses unknown[] type, which bypasses TypeScript's type checking. This could lead to runtime errors if the array contents are accessed without proper type guards.

Consider defining an Airqloud interface and using it instead:

-  airqlouds: unknown[];
+  airqlouds: Airqloud[];

Also applies to: 12-38

createdAt: string;
updatedAt?: string;
}

interface SiteCategory {
tags: string[];
area_name: string;
category: string;
highway: string;
landuse: string;
latitude: number;
longitude: number;
natural: string;
search_radius: number;
waterway: string;
}

export interface Device {
_id: string;
group: string;
authRequired: boolean;
serial_number: string;
api_code: string;
groups: string[];
}
81 changes: 81 additions & 0 deletions netmanager-app/components/Analytics/AnalyticsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client'

import React, { useState } from 'react'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Grid } from '@/app/types/grids'
import { Cohort } from '@/app/types/cohorts'

interface AnalyticsAirqloudsDropDownProps {
isCohort: boolean
airqloudsData?: Cohort[] | Grid[]
onSelect: (id: string) => void
selectedId: string | null
}

const AnalyticsAirqloudsDropDown =({
isCohort,
airqloudsData = [],
onSelect,
selectedId
}: AnalyticsAirqloudsDropDownProps) => {

const handleAirqloudChange = (value: string) => {
const selectedAirqloud = airqloudsData.find((a) => a._id === value);
if (selectedAirqloud) {
const storageKey = isCohort ? "activeCohort" : "activeGrid";
localStorage.setItem(storageKey, JSON.stringify(selectedAirqloud));
onSelect(value);
}
};
Comment on lines +22 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for localStorage operations.

The handleAirqloudChange function should handle potential localStorage failures, which can occur in various scenarios (e.g., storage quota exceeded, private browsing mode).

 const handleAirqloudChange = (value: string) => {
   const selectedAirqloud = airqloudsData.find((a) => a._id === value);
   if (selectedAirqloud) {
     const storageKey = isCohort ? "activeCohort" : "activeGrid";
-    localStorage.setItem(storageKey, JSON.stringify(selectedAirqloud));
-    onSelect(value);
+    try {
+      localStorage.setItem(storageKey, JSON.stringify(selectedAirqloud));
+      onSelect(value);
+    } catch (error) {
+      console.error('Failed to save selection to localStorage:', error);
+      onSelect(value); // Still update the UI even if storage fails
+    }
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleAirqloudChange = (value: string) => {
const selectedAirqloud = airqloudsData.find((a) => a._id === value);
if (selectedAirqloud) {
const storageKey = isCohort ? "activeCohort" : "activeGrid";
localStorage.setItem(storageKey, JSON.stringify(selectedAirqloud));
onSelect(value);
}
};
const handleAirqloudChange = (value: string) => {
const selectedAirqloud = airqloudsData.find((a) => a._id === value);
if (selectedAirqloud) {
const storageKey = isCohort ? "activeCohort" : "activeGrid";
try {
localStorage.setItem(storageKey, JSON.stringify(selectedAirqloud));
onSelect(value);
} catch (error) {
console.error('Failed to save selection to localStorage:', error);
onSelect(value); // Still update the UI even if storage fails
}
}
};


const formatString = (string: string) => {
return string
.replace(/_/g, ' ')
.replace(/\w\S*/g, (txt) => {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
})
.replace('Id', 'ID')
}

return (
<div className="relative w-full">
{airqloudsData.length === 0 ? (
<p className="text-gray-500">{isCohort ? 'No Cohorts' : 'No Grids'} data available</p>
) : (
<Select onValueChange={handleAirqloudChange} value={selectedId || undefined}>
<SelectTrigger className="w-full h-11 bg-white border-gray-200 text-blue-600 font-bold capitalize">
<SelectValue placeholder={`Select a ${isCohort ? 'Cohort' : 'Grid'}`}>
{selectedId && formatString(airqloudsData.find(a => a._id === selectedId)?.name || '')}
</SelectValue>
</SelectTrigger>
<SelectContent>
{airqloudsData.map((airqloud) => (
<SelectItem
key={airqloud._id}
value={airqloud._id}
className="cursor-pointer"
>
<div className="grid grid-cols-12 gap-2 w-full items-center">
<div className="col-span-9">
<span className="font-medium truncate ">{formatString(airqloud.name)}</span>
</div>
<div className="col-span-3 text-sm text-gray-500 text-right">
<span className=" text-sm text-gray-500 text-right">
{isCohort
? 'devices' in airqloud
? `${airqloud.devices?.length || 0} devices`
: ''
: 'sites' in airqloud ? `${airqloud.sites?.length || 0} sites` : ''}
</span>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)
}

export default AnalyticsAirqloudsDropDown;
Loading
Loading