Skip to content

Commit

Permalink
Merge pull request #899 from Health-Informatics-UoN/feat/607/projects…
Browse files Browse the repository at this point in the history
…-pages

Add Projects pages: Index and Details
  • Loading branch information
AndrewThien authored Oct 28, 2024
2 parents fb5e390 + 271c86c commit 727be6d
Show file tree
Hide file tree
Showing 29 changed files with 1,720 additions and 177 deletions.
1 change: 1 addition & 0 deletions app/api/datasets/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Meta:
"visibility",
"created_at",
"hidden",
"projects",
)


Expand Down
1 change: 1 addition & 0 deletions app/api/datasets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class DatasetAndDataPartnerListView(GenericAPIView, ListModelMixin):
"id": ["in"],
"hidden": ["in", "exact"],
"name": ["in", "icontains"],
"project": ["exact"],
}
ordering = "-created_at"

Expand Down
21 changes: 10 additions & 11 deletions app/api/projects/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from drf_dynamic_fields import DynamicFieldsMixin # type: ignore
from rest_framework import serializers
from shared.mapping.models import Project
from django.contrib.auth.models import User


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "username")


class ProjectSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
Expand All @@ -9,9 +16,11 @@ class ProjectSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
where User is permitted to view a particular Project.
"""

members = UserSerializer(read_only=True, many=True)

class Meta:
model = Project
fields = "__all__"
fields = ["id", "name", "members", "created_at"]


class ProjectNameSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
Expand All @@ -24,16 +33,6 @@ class Meta:
fields = ["id", "name"]


class ProjectWithMembersSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
Serialiser for showing the names and members of Projects. Use in non-admin ListViews.
"""

class Meta:
model = Project
fields = ["id", "name", "members"]


class ProjectDatasetSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
Serialiser for only showing the names of Projects. Use in non-admin ListViews.
Expand Down
17 changes: 8 additions & 9 deletions app/api/projects/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from projects.serializers import (
ProjectDatasetSerializer,
ProjectSerializer,
ProjectWithMembersSerializer,
)
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from shared.mapping.models import Project
from shared.mapping.permissions import CanViewProject
from api.paginations import CustomPagination
from rest_framework.filters import OrderingFilter


class ProjectList(ListAPIView):
Expand All @@ -16,19 +17,17 @@ class ProjectList(ListAPIView):
"""

permission_classes = [IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = {"name": ["in", "exact"]}
filter_backends = [DjangoFilterBackend, OrderingFilter]
pagination_class = CustomPagination
filterset_fields = {"name": ["in", "icontains"]}
ordering_fields = ["id", "name"]
ordering = "-created_at"

def get_serializer_class(self):
if (
self.request.GET.get("name") is not None
or self.request.GET.get("name__in") is not None
):
return ProjectSerializer
if self.request.GET.get("datasets") is not None:
return ProjectDatasetSerializer

return ProjectWithMembersSerializer
return ProjectSerializer

def get_queryset(self):
if dataset := self.request.GET.get("dataset"):
Expand Down
11 changes: 0 additions & 11 deletions app/next-client-app/api/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const fetchKeys = {
: "v2/datasets/",
dataPartners: () => "v2/datapartners/",
users: () => "v2/usersfilter/?is_active=true",
projects: (dataset?: string) =>
dataset ? `projects/?dataset=${dataset}` : "projects/",
updateDataset: (id: number) => `v2/datasets/${id}/`,
permissions: (id: string) => `v2/datasets/${id}/permissions/`,
create: "v2/datasets/",
Expand Down Expand Up @@ -88,15 +86,6 @@ export async function getDataUsers(): Promise<User[]> {
}
}

export async function getProjects(dataset?: string): Promise<Project[]> {
try {
return request<Project[]>(fetchKeys.projects(dataset));
} catch (error) {
console.warn("Failed to fetch data.");
return [];
}
}

export async function archiveDataSets(id: number, hidden: boolean) {
try {
await request(fetchKeys.updateDataset(id), {
Expand Down
50 changes: 50 additions & 0 deletions app/next-client-app/api/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use server";

import request from "@/lib/api/request";
import { fetchAllPages } from "@/lib/api/utils";

const fetchKeys = {
list: (filter?: string) => (filter ? `projects/?${filter}` : "projects/"),
projectsDataset: (dataset: string) => `projects/?dataset=${dataset}`,
project: (id: string) => `projects/${id}/`,
};

export async function getProjectsList(
filter?: string | undefined
): Promise<PaginatedResponse<Project>> {
try {
return await request<Project>(fetchKeys.list(filter));
} catch (error) {
console.warn("Failed to fetch data.");
return { count: 0, next: null, previous: null, results: [] };
}
}

export async function getAllProjects(): Promise<Project[]> {
try {
// Add a fake filter to the query to ensure the fetchAllPages call works normally
return await fetchAllPages<Project>(fetchKeys.list(" "));
} catch (error) {
console.warn("Failed to fetch all projects data");
return [];
}
}

export async function getProjectsDataset(
dataset: string
): Promise<PaginatedResponse<Project>> {
try {
return request<Project>(fetchKeys.projectsDataset(dataset));
} catch (error) {
console.warn("Failed to fetch data.");
return { count: 0, next: null, previous: null, results: [] };
}
}

export async function getProject(id: string): Promise<Project | null> {
try {
return await request<Project | null>(fetchKeys.project(id));
} catch (error) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
getDataSet,
getDataUsers,
getDatasetPermissions,
getProjects,
} from "@/api/datasets";
import { getAllProjects } from "@/api/projects";
import { DatasetForm } from "@/components/datasets/DatasetForm";

interface DataSetListProps {
Expand All @@ -19,7 +19,7 @@ export default async function DatasetDetails({
const dataset = await getDataSet(id);
const partners = await getDataPartners();
const users = await getDataUsers();
const projects = await getProjects();
const projects = await getAllProjects();
const permissions = await getDatasetPermissions(id);

return (
Expand Down
6 changes: 3 additions & 3 deletions app/next-client-app/app/(protected)/datasets/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Forbidden } from "@/components/core/Forbidden";
import { NavGroup } from "@/components/core/nav-group";
import { Folders } from "lucide-react";
import { Database } from "lucide-react";
import { Boundary } from "@/components/core/boundary";
import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton";
Expand Down Expand Up @@ -45,12 +45,12 @@ export default async function DatasetLayout({
return (
<div className="space-y-2">
<div className="flex font-semibold text-xl items-center space-x-2">
<Folders className="text-gray-500" />
<Database className="text-gray-500" />
<Link href={`/datasets`}>
<h2 className="text-gray-500 dark:text-gray-400">Datasets</h2>
</Link>
<h2 className="text-gray-500 dark:text-gray-400">{"/"}</h2>
<Folders className="text-blue-700" />
<Database className="text-blue-700" />
<h2>{dataset.name}</h2>
</div>

Expand Down
9 changes: 5 additions & 4 deletions app/next-client-app/app/(protected)/datasets/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { DataTable } from "@/components/data-table";
import { columns } from "./columns";
import { getDataPartners, getDataSets, getProjects } from "@/api/datasets";
import { getDataPartners, getDataSets } from "@/api/datasets";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { objToQuery } from "@/lib/client-utils";
import { DataTableFilter } from "@/components/data-table/DataTableFilter";
import { FilterParameters } from "@/types/filter";
import { CreateDatasetDialog } from "@/components/datasets/CreateDatasetDialog";
import { Folders } from "lucide-react";
import { Database } from "lucide-react";
import { getAllProjects } from "@/api/projects";

interface DataSetListProps {
searchParams?: FilterParameters;
Expand All @@ -19,7 +20,7 @@ export default async function DataSets({ searchParams }: DataSetListProps) {
};
const combinedParams = { ...defaultParams, ...searchParams };

const projects = await getProjects();
const projects = await getAllProjects();
const dataPartnerList = await getDataPartners();
const query = objToQuery(combinedParams);
const dataset = await getDataSets(query);
Expand All @@ -28,7 +29,7 @@ export default async function DataSets({ searchParams }: DataSetListProps) {
return (
<div className="space-y-2">
<div className="flex font-semibold text-xl items-center">
<Folders className="mr-2 text-blue-700" />
<Database className="mr-2 text-blue-700" />
<h2>Datasets</h2>
</div>
<div className="my-3 justify-between">
Expand Down
50 changes: 50 additions & 0 deletions app/next-client-app/app/(protected)/projects/[id]/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import { DataTableColumnHeader } from "@/components/data-table/DataTableColumnHeader";
import { ColumnDef } from "@tanstack/react-table";
import { format } from "date-fns/format";

export const columns: ColumnDef<DataSet>[] = [
{
accessorKey: "id",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="ID" sortName="id" />
),
enableHiding: false,
enableSorting: true,
},
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" sortName="name" />
),
enableHiding: true,
enableSorting: true,
},
{
id: "Creation Date",
accessorKey: "created_at",
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title="Creation Date"
sortName="created_at"
/>
),
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const date = new Date(row.original.created_at);
return format(date, "MMM dd, yyyy h:mm a");
},
},
{
id: "Data Partner",
accessorKey: "data_partner.name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Data Partner" />
),
enableHiding: true,
enableSorting: false,
},
];
80 changes: 80 additions & 0 deletions app/next-client-app/app/(protected)/projects/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Forbidden } from "@/components/core/Forbidden";
import { NavGroup } from "@/components/core/nav-group";
import { Folders } from "lucide-react";
import { Boundary } from "@/components/core/boundary";
import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { format } from "date-fns/format";
import { InfoItem } from "@/components/core/InfoItem";
import Link from "next/link";
import { getProject } from "@/api/projects";
import { AvatarList } from "@/components/core/avatar-list";

export default async function DatasetLayout({
params,
children,
}: Readonly<{
params: { id: string };
children: React.ReactNode;
}>) {
const items = [
{
name: "Datasets",
slug: "",
iconName: "Database",
},
];

const project = await getProject(params.id);
let createdDate = new Date();

if (!project) {
return <Forbidden />;
}

createdDate = new Date(project.created_at);

return (
<div className="space-y-2">
<div className="flex font-semibold text-xl items-center space-x-2">
<Folders className="text-gray-500" />
<Link href={`/projects`}>
<h2 className="text-gray-500 dark:text-gray-400">Projects</h2>
</Link>
<h2 className="text-gray-500 dark:text-gray-400">{"/"}</h2>
<Folders className="text-orange-700" />
<h2>{project?.name}</h2>
</div>

<div className="flex flex-col md:flex-row md:items-center text-sm space-y-2 md:space-y-0 divide-y md:divide-y-0 md:divide-x divide-gray-300">
<InfoItem
label="Created"
value={format(createdDate, "MMM dd, yyyy h:mm a")}
className="py-1 md:py-0 md:pr-3"
/>
<div className="py-1 md:py-0 md:px-3 h-5 flex items-center gap-2">
Members: <AvatarList members={project?.members || []} />
</div>
</div>
{/* "Navs" group */}
<div className="flex justify-between">
<NavGroup
path={`/projects/${params.id}`}
items={[
...items.map((x) => ({
text: x.name,
slug: x.slug,
iconName: x.iconName,
})),
]}
/>
</div>
<Boundary>
{" "}
<Suspense fallback={<Skeleton className="h-full w-full" />}>
{children}
</Suspense>
</Boundary>
</div>
);
}
Loading

0 comments on commit 727be6d

Please sign in to comment.