Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# Project Description

This is the dashboard code for easily interacting with [Juno](https://github.com/GTBitsOfGood/juno), [Bits of Good](https://bitsofgood.org/)'s central microservice architecture. See the main repository for more details.
This is the dashboard code for easily interacting with [Juno](https://github.com/GTBitsOfGood/juno), [Bits of Good](https://bitsofgood.org/)'s central microservice architecture. See the main repository for more details.

## Prequisites

Expand All @@ -32,7 +32,7 @@ Installing all needed packages:
bun install
```

## Development
## Development

### Running locally

Expand Down Expand Up @@ -66,6 +66,6 @@ Now, you should be able to enter API request fields. The username and password s

or if you are using a different project, then replace with that project name.

### Components
### Components

This repository uses [shadcn/ui](https://ui.shadcn.com/) for streamlining component development.
47 changes: 45 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion components.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
}
3 changes: 1 addition & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
};
const nextConfig: NextConfig = {};

export default nextConfig;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
Expand Down
5 changes: 3 additions & 2 deletions src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ const LoginPage = () => {

async function handleLoginSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError("");
setLoading(true);

const values = loginForm.getValues();
const result = await createJWTAuthentication({
email: values.email,
password: values.password,
});
if (result.success) {
router.push("/admin");
if (result.success && result.redirectPath) {
router.push(result.redirectPath);
} else {
setError(result.error);
setLoading(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use client";

import { EmailDomainTable } from "@/components/emailDomainTable/emailDomainTable";
import { EmailSenderTable } from "@/components/emailSenderTable/emailSenderTable";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { getProjectById } from "@/lib/project";
import { getEmailConfig } from "@/lib/settings";
import { useQuery } from "@tanstack/react-query";
import { ProjectResponse } from "juno-sdk/build/main/internal/index";
import { Mail, Settings } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";

const EmailConfigurationsPage = () => {
const { projectId } = useParams<{ projectId: string }>();

const [hasEmailConfig, setHasEmailConfig] = useState(null);
const [emailConfigLoading, setEmailConfigLoading] = useState(true);

const {
isLoading,
isError,
data: project,
error,
} = useQuery<ProjectResponse>({
queryKey: ["project", projectId],
queryFn: async () => {
const result = await getProjectById(Number(projectId));
if (!result.success) {
throw new Error(result.error);
}
return result.project;
},
});

if (isError) {
toast.error("Error", {
description: `Failed to fetch project: ${JSON.stringify(error)}`,
});
}
Comment thread
shaply marked this conversation as resolved.
Outdated

useEffect(() => {
const loadEmailConfig = async () => {
try {
const configRes = await getEmailConfig(String(projectId));
if (configRes) {
setHasEmailConfig(configRes);
}
} catch (e) {
console.error("Error loading email config:", e);
toast.error("Error loading email config", {
description: "Please try again later",
});
} finally {
setEmailConfigLoading(false);
}
};

loadEmailConfig();
}, [projectId]);

const breadcrumb = (
<Breadcrumb className="mb-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/projects">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/projects/${projectId}`}>
{isLoading ? "****" : (project?.name ?? "Unknown")}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/projects/${projectId}/services/email`}>
Email
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Configurations</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);

if (emailConfigLoading) {
return (
<div className="flex flex-col">
{breadcrumb}
<Separator className="mb-8" />
<h1 className="mb-4 text-lg font-bold">Email Configurations</h1>
<div className="space-y-4">
<div className="h-64 animate-pulse rounded-md bg-muted" />
<div className="h-64 animate-pulse rounded-md bg-muted" />
</div>
</div>
);
}

if (!hasEmailConfig) {
return (
<div className="flex flex-col">
{breadcrumb}
<Separator className="mb-8" />
<h1 className="mb-4 text-lg font-bold">Email Configurations</h1>
<Card className="max-w-[35%]">
<CardHeader>
<div className="flex items-center gap-3">
<Mail className="h-5 w-5 text-muted-foreground" />
<CardTitle>No Email Configuration</CardTitle>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
Configure your email service first to manage senders and domains.
</p>
<Button asChild>
<Link href={`/projects/${projectId}/settings`}>
<Settings className="mr-2 h-4 w-4" />
Go to Settings
</Link>
</Button>
</CardContent>
</Card>
</div>
);
}

return (
<div className="flex flex-col">
{breadcrumb}
<Separator className="mb-8" />
<div className="flex flex-col gap-8">
<EmailSenderTable projectId={projectId} />
<EmailDomainTable projectId={projectId} />
</div>
</div>
);
};

export default EmailConfigurationsPage;
5 changes: 2 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { redirect } from "next/navigation";

export default function Home() {
redirect("/admin");
// middleware handles redirect to correct route
return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,26 @@ interface AnalyticsActionsCellProps {
clientAnalyticsKey: string;
},
) => void;
isReadOnly: boolean;
}

export const AnalyticsActionsCell = ({
config,
projectId,
isPending,
onUpdateConfig,
isReadOnly,
}: AnalyticsActionsCellProps) => {
return (
<>
<Dialog modal={false}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isReadOnly}
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
Expand Down
3 changes: 3 additions & 0 deletions src/components/analyticsConfigTable/analyticsConfig-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { useReadOnlyMode } from "../providers/SessionProvider";
import { BaseTable } from "../baseTable";
import AddAnalyticsConfigForm from "../forms/AddAnalyticsConfigForm";
import { Button } from "../ui/button";
Expand All @@ -29,6 +30,7 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
const [isAddConfigDialogOpen, setIsAddConfigDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const isReadOnly = useReadOnlyMode();

const queryClient = useQueryClient();

Expand Down Expand Up @@ -181,6 +183,7 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
});
setIsAddConfigDialogOpen(false);
},
isReadOnly,
)}
isLoading={isLoading}
filterParams={{
Expand Down
4 changes: 4 additions & 0 deletions src/components/analyticsConfigTable/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const analyticsConfigColumns = (
clientAnalyticsKey: string;
},
) => void,
isReadOnly: boolean,
): ColumnDef<AnalyticsConfig>[] => {
return [
{
Expand All @@ -29,13 +30,15 @@ export const analyticsConfigColumns = (
<Checkbox
className="ms-2 align-middle mr-5"
checked={table.getIsAllPageRowsSelected()}
disabled={isReadOnly}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
cell: ({ row }) => (
<Checkbox
className="ms-2 align-middle"
checked={row.getIsSelected()}
disabled={isReadOnly}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
Expand Down Expand Up @@ -68,6 +71,7 @@ export const analyticsConfigColumns = (
projectId={projectId}
onUpdateConfig={onUpdateConfig}
isPending={isPending}
isReadOnly={isReadOnly}
/>
);
},
Expand Down
10 changes: 9 additions & 1 deletion src/components/baseTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "@tanstack/react-table";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { useReadOnlyMode } from "./providers/SessionProvider";
import SkeletonRows from "./table/SkeletonRows";
import { Button } from "./ui/button";

Expand All @@ -46,6 +47,7 @@ export function BaseTable<TData, TValue>({
onDeleteRow,
}: BaseTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const isReadOnly = useReadOnlyMode();

const table = useReactTable({
data,
Expand Down Expand Up @@ -81,7 +83,9 @@ export function BaseTable<TData, TValue>({
{selectedRows.length > 0 && onDeleteRow && (
<Button
variant="destructive"
disabled={isReadOnly}
onClick={() => {
if (isReadOnly) return;
onDeleteRow(selectedRows);
table.resetRowSelection();
}}
Expand All @@ -90,7 +94,11 @@ export function BaseTable<TData, TValue>({
{selectedRows.length > 1 ? "s" : ""}
</Button>
)}
{onAddNewRow && <Button onClick={onAddNewRow}>Add New</Button>}
{onAddNewRow && (
<Button disabled={isReadOnly} onClick={onAddNewRow}>
Add New
</Button>
)}
</div>
<div className="rounded-md border">
<Table>
Expand Down
4 changes: 4 additions & 0 deletions src/components/emailConfigTable/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const emailConfigColumns = (
projectId: number,
isPending: boolean,
onAddConfig: (sendgridKey: string) => void,
isReadOnly: boolean,
): ColumnDef<EmailConfig>[] => {
return [
{
Expand All @@ -25,13 +26,15 @@ export const emailConfigColumns = (
<Checkbox
className="ms-2 align-middle mr-5"
checked={table.getIsAllPageRowsSelected()}
disabled={isReadOnly}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
),
cell: ({ row }) => (
<Checkbox
className="ms-2 align-middle"
checked={row.getIsSelected()}
disabled={isReadOnly}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
),
Expand Down Expand Up @@ -75,6 +78,7 @@ export const emailConfigColumns = (
projectId={projectId}
onAddConfig={onAddConfig}
isPending={isPending}
isReadOnly={isReadOnly}
/>
);
},
Expand Down
Loading
Loading