-
Notifications
You must be signed in to change notification settings - Fork 31
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
base: staging
Are you sure you want to change the base?
Changes from all commits
cea9cd8
f67563a
272627d
b1a0555
c8d7198
c9999a0
ccc664d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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", | ||
}, | ||
]; | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
+ );
+ });
|
||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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>
|
||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace sample data with API integration.
The component uses hardcoded sample data. This should be replaced with actual data from an API endpoint.