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

[Netmanager-app]improved on the grids hooks #2544

Open
wants to merge 6 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
43 changes: 43 additions & 0 deletions src/netmanager-app/components/Analytics/analytics-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type React from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"

export const AnalyticsSkeleton: React.FC = () => {
return (
<Card className="p-4">
<CardContent className="space-y-6">
{/* Header section skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-12 gap-4 mb-6">
<div className="col-span-1 sm:col-span-2 lg:col-span-6 mb-4 sm:mb-0">
<Skeleton className="h-10 w-full rounded-md" />
</div>
<div className="col-span-1 sm:col-span-2 lg:col-span-6 flex flex-col sm:flex-row justify-start sm:justify-end items-stretch sm:items-center gap-4">
<Skeleton className="h-10 w-32 sm:w-36" />
<Skeleton className="h-10 w-36 sm:w-40" />
<Skeleton className="h-10 w-10" />
</div>
</div>

{/* Dashboard content skeleton */}
<div className="space-y-4">
{/* Chart skeletons */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Skeleton className="h-64 w-full rounded-md" />
<Skeleton className="h-64 w-full rounded-md" />
</div>

{/* Table skeleton */}
<Skeleton className="h-80 w-full rounded-md" />

{/* Additional metrics skeletons */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Skeleton className="h-32 w-full rounded-md" />
<Skeleton className="h-32 w-full rounded-md" />
<Skeleton className="h-32 w-full rounded-md" />
</div>
</div>
</CardContent>
</Card>
)
}

186 changes: 83 additions & 103 deletions src/netmanager-app/components/Analytics/index.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,75 @@
"use client";

import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ImportIcon, MoreHorizontal } from "lucide-react";
import AnalyticsAirqloudsDropDown from "./AnalyticsDropdown";
import GridDashboard from "./GridDashboard";
import CohortDashboard from "./CohortsDashboard";
import { useGrids } from "@/core/hooks/useGrids";
import { useCohorts } from "@/core/hooks/useCohorts";
import { dataExport } from "@/core/apis/analytics";
import { useToast } from "@/components/ui/use-toast";
import { useAppSelector } from "@/core/redux/hooks";
import { Cohort } from "@/app/types/cohorts";
import { Grid } from "@/app/types/grids";
import { useMapReadings } from "@/core/hooks/useDevices";
import { transformDataToGeoJson } from "@/lib/utils";
"use client"

import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { ImportIcon, MoreHorizontal, Loader2 } from "lucide-react"
import AnalyticsAirqloudsDropDown from "./AnalyticsDropdown"
import GridDashboard from "./GridDashboard"
import CohortDashboard from "./CohortsDashboard"
import { useGrids } from "@/core/hooks/useGrids"
import { useCohorts } from "@/core/hooks/useCohorts"
import { dataExport } from "@/core/apis/analytics"
import { useToast } from "@/components/ui/use-toast"
import type { Cohort } from "@/app/types/cohorts"
import type { Grid } from "@/app/types/grids"
import { useMapReadings } from "@/core/hooks/useDevices"
// import { AnalyticsSkeleton } from "./analytics-skeleton"

const NewAnalytics: React.FC = () => {
const [isCohort, setIsCohort] = useState(false);
const [downloadingData, setDownloadingData] = useState(false);
const [activeGrid, setActiveGrid] = useState<Grid>();
const [activeCohort, setActiveCohort] = useState<Cohort>();
const activeNetwork = useAppSelector((state) => state.user.activeNetwork);
const [isCohort, setIsCohort] = useState(false)
const [downloadingData, setDownloadingData] = useState(false)
const [activeGrid, setActiveGrid] = useState<Grid>()
const [activeCohort, setActiveCohort] = useState<Cohort>()
const [transformedReadings, setTransformedReadings] = useState<{
type: string;
type: string
features: {
type: "Feature";
properties: unknown;
geometry: { type: "Point"; coordinates: [number, number] };
}[]; // Updated type
} | null>(null);
type: "Feature"
properties: unknown
geometry: { type: "Point"; coordinates: [number, number] }
}[] // Updated type
} | null>(null)

const { toast } = useToast();
const { grids, isLoading: isGridsLoading } = useGrids(
activeNetwork?.net_name ?? ""
);
const { cohorts, isLoading: isCohortsLoading } = useCohorts();
const { mapReadings, isLoading } = useMapReadings();
const { toast } = useToast()
const { grids, isLoading: isGridsLoading } = useGrids()
const { cohorts, isLoading: isCohortsLoading } = useCohorts()
const { mapReadings, isLoading: isMapReadingsLoading } = useMapReadings()

const airqloudsData = isCohort ? cohorts : grids;
const airqloudsData = isCohort ? cohorts : grids
const isLoading = isGridsLoading || isCohortsLoading || isMapReadingsLoading

const handleAirqloudSelect = (id: string) => {
const selectedData = isCohort
? cohorts.find((cohort: Cohort) => cohort._id === id)
: grids.find((grid: Grid) => grid._id === id);
: grids.find((grid: Grid) => grid._id === id)
if (selectedData) {
const storageKey = isCohort ? "activeCohort" : "activeGrid";
const setActive = isCohort ? setActiveCohort : setActiveGrid;
setActive(selectedData);
localStorage.setItem(storageKey, JSON.stringify(selectedData));
const storageKey = isCohort ? "activeCohort" : "activeGrid"
const setActive = isCohort ? setActiveCohort : setActiveGrid
setActive(selectedData)
localStorage.setItem(storageKey, JSON.stringify(selectedData))
}
};
}

useEffect(() => {
const activeGrid = localStorage.getItem("activeGrid");
const activeCohort = localStorage.getItem("activeCohort");
const activeGrid = localStorage.getItem("activeGrid")
const activeCohort = localStorage.getItem("activeCohort")

setActiveGrid(
activeGrid && Array.isArray(grids) && grids.length > 0
? JSON.parse(activeGrid)
: Array.isArray(grids) && grids.length > 0
? grids[0]
: null
);
? grids[0]
: null,
)

setActiveCohort(
activeCohort && Array.isArray(cohorts) && cohorts.length > 0
? JSON.parse(activeCohort)
: (cohorts && cohorts[0]) || null
);
}, [grids, cohorts]);
: (cohorts && cohorts[0]) || null,
)
}, [grids, cohorts])

// useEffect(()=>{
// if(mapReadings) {
Expand All @@ -97,57 +90,57 @@ const NewAnalytics: React.FC = () => {
// }, [mapReadings]);

const handleSwitchGridsCohort = () => {
setIsCohort(!isCohort);
};
setIsCohort(!isCohort)
}

const handleDownloadData = async () => {
setDownloadingData(true);
setDownloadingData(true)
try {
await dataExport({
...(isCohort
? {}
: { sites: activeGrid?.sites.map((site) => site._id) || [] }),
...(isCohort ? {} : { sites: activeGrid?.sites.map((site) => site._id) || [] }),
...(isCohort
? {
device_names:
activeCohort?.devices.map((device) => device.name) || [],
device_names: activeCohort?.devices.map((device) => device.name) || [],
}
: {}),
startDateTime: new Date(
Date.now() - 5 * 24 * 60 * 60 * 1000
).toISOString(),
startDateTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
endDateTime: new Date().toISOString(),
frequency: "hourly",
pollutants: ["pm2_5", "pm10"],
downloadType: isCohort ? "csv" : "json",
outputFormat: "airqo-standard",
minimum: true,
datatype: "raw",
});
})
toast({
title: "Success",
description: "Air quality data download successful",
});
})
} catch (error) {
toast({
title: "Error",
description:
error instanceof Error
? error.message
: "Failed to download data. Please try again.",
description: error instanceof Error ? error.message : "Failed to download data. Please try again.",
variant: "destructive",
});
})
} finally {
setDownloadingData(false);
setDownloadingData(false)
}
};
}

const handleRefreshGrid = async () => {
toast({
title: "Refresh",
description: "Grid refresh initiated",
});
};
})
}

if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
Comment on lines +137 to +143
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 using the AnalyticsSkeleton component.

You've created a nice AnalyticsSkeleton component but are using a simple spinner here. For better user experience, consider using the skeleton UI which would provide a closer representation of the content being loaded.

-  if (isLoading) {
-    return (
-      <div className="flex items-center justify-center min-h-screen">
-        <Loader2 className="w-8 h-8 animate-spin" />
-      </div>
-    );
-  }
+  if (isLoading) {
+    return <AnalyticsSkeleton />
+  }
📝 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
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (isLoading) {
return <AnalyticsSkeleton />
}


return (
<Card className="p-4">
Expand All @@ -158,42 +151,32 @@ const NewAnalytics: React.FC = () => {
isCohort={isCohort}
airqloudsData={airqloudsData}
onSelect={handleAirqloudSelect}
selectedId={
isCohort ? activeCohort?._id ?? null : activeGrid?._id ?? null
}
selectedId={isCohort ? (activeCohort?._id ?? null) : (activeGrid?._id ?? null)}
/>
</div>
<div className="col-span-1 sm:col-span-2 lg:col-span-6 flex flex-col sm:flex-row justify-start sm:justify-end items-stretch sm:items-center gap-4">
<Button
variant="outline"
onClick={handleDownloadData}
disabled={downloadingData || isGridsLoading || isCohortsLoading}
disabled={downloadingData || isLoading}
className="w-full sm:w-auto"
>
Download Data
{downloadingData && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{downloadingData ? "Downloading..." : "Download Data"}
</Button>
<Button
onClick={handleSwitchGridsCohort}
className="w-full sm:w-auto"
>
<Button onClick={handleSwitchGridsCohort} className="w-full sm:w-auto">
<ImportIcon className="mr-2 h-4 w-4" />
Switch to {isCohort ? "Grid View" : "Cohort View"}
</Button>
{!isCohort && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="w-full sm:w-auto"
>
<Button variant="outline" size="icon" className="w-full sm:w-auto">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleRefreshGrid}>
Refresh Grid
</DropdownMenuItem>
<DropdownMenuItem onClick={handleRefreshGrid}>Refresh Grid</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
Expand All @@ -213,15 +196,12 @@ const NewAnalytics: React.FC = () => {
/>
)}
{isCohort && activeCohort && (
<CohortDashboard
loading={isCohortsLoading}
cohortId={activeCohort._id}
cohorts={cohorts}
/>
<CohortDashboard loading={isCohortsLoading} cohortId={activeCohort._id} cohorts={cohorts} />
)}
</CardContent>
</Card>
);
};
)
}

export default NewAnalytics

export default NewAnalytics;
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function CreateOrganizationDialog() {
};

const onInviteMembers = async (data: z.infer<typeof inviteMembersSchema>) => {
// const groupId = createOrgForm.getValues().grp_title

try {
for (const member of data.members) {
await inviteUserToGroup(member.email);
Expand Down
17 changes: 7 additions & 10 deletions src/netmanager-app/core/hooks/useGrids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { AxiosError } from "axios";
import { grids } from "../apis/grids";
import { CreateGrid, Grid } from "@/app/types/grids";
import { GridsState, setError, setGrids } from "../redux/slices/gridsSlice";
import { setError, setGrids } from "../redux/slices/gridsSlice";
import { useDispatch } from "react-redux";
import { useAppSelector } from "../redux/hooks";

Expand All @@ -16,24 +16,21 @@ interface ErrorResponse {
}

// Hook to get the grid summary
export const useGrids = (networkId: string) => {
export const useGrids = () => {
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Hook signature simplified - verify impact on consumers.

The function signature has been changed to remove the networkId parameter. This simplifies usage but may affect any components that were passing a non-active network ID.


🏁 Script executed:

#!/bin/bash
# Check for any instances where useGrids is called with parameters
rg "useGrids\(\S" --type tsx --type ts

Length of output: 68


Below is a revised verification step to further check for any consumers that might still be passing parameters to the updated hook signature:

Next Steps:

  • Please run the script above so we can confirm if any components are still calling useGrids with an argument.
  • If the output shows any instances, those usages will need to be updated to remove the obsolete networkId parameter.
  • If nothing is returned, it’s still a good idea to do a manual scan—especially in TSX files—to ensure there are no unintended regressions.

This additional verification is important because the earlier command failed due to the unrecognized file type "tsx". Once you’ve checked that no consumers pass parameters, the hook signature change should be safe.

Thanks for double-checking!


🏁 Script executed:

#!/bin/bash
# Check for any instances where useGrids is called with non-empty arguments
# (i.e. where there's something between the parentheses other than just whitespace).
rg "useGrids\\(\\s*[^)]" -g "*.ts" -g "*.tsx"

Length of output: 285


Action Required: Update Consumer Usage of useGrids

It appears that although the hook signature was simplified (removing the networkId parameter), there is still a consumer in the codebase invoking it with an argument:

  • File: src/netmanager-app/components/export-data/ExportForm.tsx
    Usage: const { grids } = useGrids(activeNetwork?.net_name || "");

Please remove the argument from this call to align with the updated hook signature. Verify that this change doesn't affect the component's logic or any dependent behavior.

const dispatch = useDispatch();
const activeNetwork = useAppSelector((state) => state.user.activeNetwork);

const { data, isLoading, error } = useQuery<
GridsState,
AxiosError<ErrorResponse>
>({
const { data, isLoading, error } = useQuery({
queryKey: ["grids", activeNetwork?.net_name],
queryFn: () => grids.getGridsApi(networkId),
queryFn: () => grids.getGridsApi(activeNetwork?.net_name || ""),
enabled: !!activeNetwork?.net_name,
onSuccess: (data: GridsState) => {
onSuccess: (data: any) => {
dispatch(setGrids(data.grids));
},
onError: (error: AxiosError<ErrorResponse>) => {
onError: (error: Error) => {
dispatch(setError(error.message));
},
} as UseQueryOptions<GridsState, AxiosError<ErrorResponse>>);
});

return {
grids: data?.grids ?? [],
Expand Down
Loading