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] Grids and Cohorts pages & functionality #2418

Open
wants to merge 7 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
263 changes: 263 additions & 0 deletions netmanager-app/app/(authenticated)/cohorts/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { ChevronLeft, Copy, Search, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { AddDevicesDialog } from "@/components/cohorts/assign-cohort-devices";

// Sample cohort data
const cohortData = {
name: "victoria_sugar",
id: "675bd462c06188001333d4d5",
visibility: "true",
};

// Sample devices data
const devices = [
{
name: "Aq_29",
description: "AIRQO UNIT with PMS5003 Victoria S",
site: "N/A",
deploymentStatus: "Deployed",
dateCreated: "2019-03-02T00:00:00.000Z",
},
{
name: "Aq_34",
description: "AIRQO UNIT with PMS5003 Victoria S",
site: "N/A",
deploymentStatus: "Deployed",
dateCreated: "2019-03-14T00:00:00.000Z",
},
{
name: "Aq_35",
description: "AIRQO UNIT with PMS5003 Victoria S",
site: "N/A",
deploymentStatus: "Deployed",
dateCreated: "2019-03-28T00:00:00.000Z",
},
];
Comment on lines +28 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace sample data with API integration.

The component uses hardcoded sample data. This should be replaced with actual data from an API endpoint.

-// Sample cohort data
-const cohortData = {
-  name: "victoria_sugar",
-  id: "675bd462c06188001333d4d5",
-  visibility: "true",
-};
-
-// Sample devices data
-const devices = [
-  {
-    name: "Aq_29",
-    description: "AIRQO UNIT with PMS5003 Victoria S",
-    site: "N/A",
-    deploymentStatus: "Deployed",
-    dateCreated: "2019-03-02T00:00:00.000Z",
-  },
-  // ...
-];
+interface Device {
+  name: string;
+  description: string;
+  site: string;
+  deploymentStatus: string;
+  dateCreated: string;
+}
+
+interface CohortData {
+  name: string;
+  id: string;
+  visibility: string;
+}

Committable suggestion skipped: line range outside the PR's diff.


export default function CohortDetailsPage() {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const [cohortDetails, setCohortDetails] = useState(cohortData);

const handleCopyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};

const handleReset = () => {
setCohortDetails(cohortData);
};

const handleSave = () => {
console.log("Saving changes:", cohortDetails);
};

const filteredDevices = devices.filter((device) =>
Object.values(device).some((value) =>
String(value).toLowerCase().includes(searchQuery.toLowerCase())
)
);
Comment on lines +76 to +80
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize device filtering for better performance.

The current filtering implementation checks every value of every device object, which could be inefficient for large datasets. Consider filtering only on relevant fields.

-  const filteredDevices = devices.filter((device) =>
-    Object.values(device).some((value) =>
-      String(value).toLowerCase().includes(searchQuery.toLowerCase())
-    )
-  );
+  const filteredDevices = devices.filter((device) => {
+    const searchLower = searchQuery.toLowerCase();
+    return (
+      device.name.toLowerCase().includes(searchLower) ||
+      device.description.toLowerCase().includes(searchLower) ||
+      device.site.toLowerCase().includes(searchLower)
+    );
+  });

Committable suggestion skipped: line range outside the PR's diff.


const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
};

return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<Button variant="ghost" className="gap-2" onClick={() => router.back()}>
<ChevronLeft className="h-4 w-4" />
Cohort Details
</Button>
<AddDevicesDialog />
</div>

<div className="grid gap-6">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="cohortName">Cohort name *</Label>
<Input
id="cohortName"
value={cohortDetails.name}
onChange={(e) =>
setCohortDetails({ ...cohortDetails, name: e.target.value })
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cohortId">Cohort ID *</Label>
<div className="flex gap-2">
<Input id="cohortId" value={cohortDetails.id} readOnly />
<Button
variant="outline"
size="icon"
onClick={() => handleCopyToClipboard(cohortDetails.id)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>

<div className="space-y-2">
<Label htmlFor="visibility">Visibility *</Label>
<Select
value={cohortDetails.visibility}
onValueChange={(value) =>
setCohortDetails({ ...cohortDetails, visibility: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Select visibility" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
</div>

<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Recent Measurements API</Label>
<div className="flex gap-2">
<Input
value="https://api.airqo.net/api/v2/devices/measurements"
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={() =>
handleCopyToClipboard(
"https://api.airqo.net/api/v2/devices/measurements"
)
}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label>Historical Measurements API</Label>
<div className="flex gap-2">
<Input
value="https://api.airqo.net/api/v2/devices/measurements"
readOnly
className="font-mono text-sm"
/>
<Button
variant="outline"
size="icon"
onClick={() =>
handleCopyToClipboard(
"https://api.airqo.net/api/v2/devices/measurements"
)
}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>

<div className="flex justify-end gap-2">
<Button variant="outline" onClick={handleReset}>
Reset
</Button>
<Button onClick={handleSave}>Save Changes</Button>
</div>

<div className="space-y-4">
<h2 className="text-lg font-semibold">Cohort devices</h2>
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search devices..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>

<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Site</TableHead>
<TableHead>Deployment status</TableHead>
<TableHead>Date created</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDevices.map((device) => (
<TableRow key={device.name}>
<TableCell className="font-medium">{device.name}</TableCell>
<TableCell>{device.description}</TableCell>
<TableCell>{device.site}</TableCell>
<TableCell>
<Badge
variant={
device.deploymentStatus === "Deployed"
? "default"
: "secondary"
}
>
{device.deploymentStatus}
</Badge>
</TableCell>
<TableCell>{formatDate(device.dateCreated)}</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
Comment on lines +246 to +253
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add confirmation dialog for device deletion.

The delete button lacks a confirmation dialog, which could lead to accidental deletions.

+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";

-<Button
-  variant="ghost"
-  size="icon"
-  className="text-destructive hover:text-destructive"
->
-  <Trash2 className="h-4 w-4" />
-</Button>
+<AlertDialog>
+  <AlertDialogTrigger asChild>
+    <Button
+      variant="ghost"
+      size="icon"
+      className="text-destructive hover:text-destructive"
+    >
+      <Trash2 className="h-4 w-4" />
+    </Button>
+  </AlertDialogTrigger>
+  <AlertDialogContent>
+    <AlertDialogHeader>
+      <AlertDialogTitle>Are you sure?</AlertDialogTitle>
+      <AlertDialogDescription>
+        This action cannot be undone. This will permanently remove the device
+        from this cohort.
+      </AlertDialogDescription>
+    </AlertDialogHeader>
+    <AlertDialogFooter>
+      <AlertDialogCancel>Cancel</AlertDialogCancel>
+      <AlertDialogAction onClick={() => handleDeviceDelete(device.name)}>
+        Delete
+      </AlertDialogAction>
+    </AlertDialogFooter>
+  </AlertDialogContent>
+</AlertDialog>

Committable suggestion skipped: line range outside the PR's diff.

</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</div>
</div>
);
}
128 changes: 128 additions & 0 deletions netmanager-app/app/(authenticated)/cohorts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use client";

import { useState } from "react";
import { Search } from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { useRouter } from "next/navigation";
import { CreateCohortDialog } from "@/components/cohorts/create-cohort";

// Sample data
const cohorts = [
{
name: "victoria_sugar",
numberOfDevices: 5,
visibility: true,
dateCreated: "2024-12-13T06:29:54.490Z",
},
{
name: "nairobi_mobile",
numberOfDevices: 4,
visibility: false,
dateCreated: "2024-10-27T18:10:41.672Z",
},
{
name: "car_free_day_demo",
numberOfDevices: 3,
visibility: true,
dateCreated: "2024-09-07T07:00:00.956Z",
},
{
name: "nimr",
numberOfDevices: 4,
visibility: false,
dateCreated: "2024-01-31T05:32:52.642Z",
},
{
name: "map",
numberOfDevices: 10,
visibility: true,
dateCreated: "2024-01-23T09:42:50.735Z",
},
];

export default function CohortsPage() {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");

const filteredCohorts = cohorts.filter((cohort) =>
cohort.name.toLowerCase().includes(searchQuery.toLowerCase())
);

const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
};

return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div className="space-y-1">
<h1 className="text-2xl font-semibold">Cohort Registry</h1>
<p className="text-sm text-muted-foreground">
Manage and organize your device cohorts
</p>
</div>
<CreateCohortDialog />
</div>

<div className="flex items-center justify-between mb-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search cohorts..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>

<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Cohort Name</TableHead>
<TableHead>Number of devices</TableHead>
<TableHead>Visibility</TableHead>
<TableHead>Date created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCohorts.map((cohort) => (
<TableRow
key={cohort.name}
className="cursor-pointer"
onClick={() => router.push(`/cohorts/${cohort.name}`)}
>
<TableCell className="font-medium">{cohort.name}</TableCell>
<TableCell>{cohort.numberOfDevices}</TableCell>
<TableCell>
<Badge variant={cohort.visibility ? "default" : "secondary"}>
{cohort.visibility ? "Visible" : "Hidden"}
</Badge>
</TableCell>
<TableCell>{formatDate(cohort.dateCreated)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
Loading