Skip to content

Commit

Permalink
run approval flow
Browse files Browse the repository at this point in the history
  • Loading branch information
psiddharthdesign committed Jul 26, 2024
1 parent dd04a30 commit f41a40c
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Tables } from "@/lib/database.types";
import { motion } from "framer-motion";
import { RunsTable } from "./RunsTable";

export default function RunDetails({ runs, project }: { runs: Tables<'digger_runs'>[], project: Tables<'projects'> }) {
export default function RunsDetails({ runs, project }: { runs: Tables<'digger_runs'>[], project: Tables<'projects'> }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
Expand All @@ -32,7 +32,7 @@ export default function RunDetails({ runs, project }: { runs: Tables<'digger_run
animate={{ opacity: 1 }}
transition={{ duration: 0.15, delay: 0.2 }}
>
<RunsTable runs={runs} />
<RunsTable runs={runs} projectSlug={project.slug} />
</motion.div>
</CardContent>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tables } from "@/lib/database.types";
import { motion } from "framer-motion";
import { AnimatePresence, motion } from "framer-motion";
import { Activity } from "lucide-react";
import moment from "moment";
import Link from "next/link";

type StatusColor = {
export type StatusColor = {
[key: string]: string;
};

const statusColors: StatusColor = {
export const statusColors: StatusColor = {
queued: 'bg-yellow-200/50 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200',
'pending_approval': 'bg-blue-200/50 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200',
running: 'bg-purple-200/50 text-purple-800 dark:bg-purple-900/50 dark:text-purple-200',
Expand All @@ -17,56 +18,81 @@ const statusColors: StatusColor = {
failed: 'bg-red-200/50 text-red-800 dark:bg-red-900/50 dark:text-red-200',
};

export const RunsTable = ({ runs }: { runs: Tables<'digger_runs'>[] }) => (
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-left">Run ID</TableHead>
<TableHead className="text-left">Commit ID</TableHead>
<TableHead className="text-left">Status</TableHead>
<TableHead className="text-left">Date</TableHead>
<TableHead className="text-left">User</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runs.length > 0 ? (
runs.map((run) => (
<TableRow key={run.id}>
<TableCell >{run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id}</TableCell>
<TableCell>{run.commit_id}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[run.status.toLowerCase()] || ''}`}>
{run.status.toUpperCase()}
</span>
</TableCell>
<TableCell>{moment(run.created_at).fromNow()}</TableCell>
<TableCell>{run.approval_author}</TableCell>
</TableRow>
))
) : (
const statusOrder = ['running', 'queued', 'pending_approval', 'approved', 'succeeded', 'failed'];

export const RunsTable = ({ runs, projectSlug }: { runs: Tables<'digger_runs'>[], projectSlug: string }) => {
const sortedRuns = [...runs].sort((a, b) => {
const statusA = statusOrder.indexOf(a.status.toLowerCase());
const statusB = statusOrder.indexOf(b.status.toLowerCase());
if (statusA !== statusB) return statusA - statusB;
return moment(b.created_at).valueOf() - moment(a.created_at).valueOf();
});

return (
<Table>
<TableHeader>
<TableRow>
<TableCell colSpan={5}>
<motion.div
className="flex flex-col items-center justify-center h-64 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
className="rounded-full bg-gray-100 p-4 dark:bg-gray-800"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Activity className="h-8 w-8 text-gray-400" />
</motion.div>
<h3 className="mt-4 text-lg font-semibold text-gray-900 dark:text-gray-100">No runs available</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Runs will appear here once they are initiated.
</p>
</motion.div>
</TableCell>
<TableHead className="text-left">Run ID</TableHead>
<TableHead className="text-left">Commit ID</TableHead>
<TableHead className="text-left">Status</TableHead>
<TableHead className="text-left">Date</TableHead>
<TableHead className="text-left">User</TableHead>
</TableRow>
)}
</TableBody>
</Table>
);
</TableHeader>
<TableBody>
<AnimatePresence>
{sortedRuns.length > 0 ? (
sortedRuns.map((run) => (
<motion.tr
key={run.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.3 }}
>
<TableCell>
<Link href={`/project/${projectSlug}/runs/${run.id}`}>
<span className="cursor-pointer hover:underline">
{run.id.length > 8 ? `${run.id.substring(0, 8)}...` : run.id}
</span>
</Link>
</TableCell>
<TableCell>{run.commit_id}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[run.status.toLowerCase()] || ''}`}>
{run.status.toUpperCase()}
</span>
</TableCell>
<TableCell>{moment(run.created_at).fromNow()}</TableCell>
<TableCell>{run.approval_author}</TableCell>
</motion.tr>
))
) : (
<TableRow>
<TableCell colSpan={5}>
<motion.div
className="flex flex-col items-center justify-center h-64 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<motion.div
className="rounded-full bg-gray-100 p-4 dark:bg-gray-800"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Activity className="h-8 w-8 text-gray-400" />
</motion.div>
<h3 className="mt-4 text-lg font-semibold text-gray-900 dark:text-gray-100">No runs available</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Runs will appear here once they are initiated.
</p>
</motion.div>
</TableCell>
</TableRow>
)}
</AnimatePresence>
</TableBody>
</Table>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ApplicationLayoutShell } from '@/components/ApplicationLayoutShell';
import { InternalNavbar } from '@/components/NavigationMenu/InternalNavbar';
import { PageHeading } from '@/components/PageHeading';
import { TabsNavigationV2 } from '@/components/TabsNavigation/TabsNavigation';
import { getProjectTitleById, getSlimProjectBySlug } from '@/data/user/projects';
import { projectSlugParamSchema } from '@/utils/zod-schemas/params';
import { Suspense, type ReactNode } from 'react';
import { PageHeading } from "@/components/PageHeading";
import { TabsNavigationV2 } from "@/components/TabsNavigation/TabsNavigation";
import { getProjectTitleById, getSlimProjectBySlug } from "@/data/user/projects";
import { projectSlugParamSchema } from "@/utils/zod-schemas/params";
import { Suspense } from "react";


async function ProjectPageHeading({ projectId }: { projectId: string }) {
Expand All @@ -16,18 +14,7 @@ async function ProjectPageHeading({ projectId }: { projectId: string }) {
/>
);
}

export default async function ProjectLayout({
params,
children,
navbar,
sidebar,
}: {
children: ReactNode;
params: unknown;
navbar: ReactNode;
sidebar: ReactNode;
}) {
export default async function ProjectPagesLayout({ params, children }: { params: unknown, children: React.ReactNode }) {
const { projectSlug } = projectSlugParamSchema.parse(params);
const project = await getSlimProjectBySlug(projectSlug);

Expand All @@ -45,28 +32,16 @@ export default async function ProjectLayout({
href: `/project/${projectSlug}/settings`,
},
];
return <>
<div className="flex flex-col">
<Suspense>
<ProjectPageHeading projectId={project.id} />
</Suspense>
<Suspense>
<TabsNavigationV2 tabs={tabs} />
</Suspense>
</div>
{children}
</>

return (

<ApplicationLayoutShell sidebar={sidebar}>
<div className="">
<InternalNavbar>
<div className="flex w-full justify-between items-center">
<Suspense>{navbar}</Suspense>
</div>
</InternalNavbar>
<div className="m-6">
<div className="flex flex-col">
<Suspense>
<ProjectPageHeading projectId={project.id} />
</Suspense>
<Suspense>
<TabsNavigationV2 tabs={tabs} />
</Suspense>
</div>
{children}
</div>
</div>
</ApplicationLayoutShell>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApplicationLayoutShell } from '@/components/ApplicationLayoutShell';
import { InternalNavbar } from '@/components/NavigationMenu/InternalNavbar';
import { Suspense, type ReactNode } from 'react';


export default async function ProjectLayout({
params,
children,
navbar,
sidebar,
}: {
children: ReactNode;
params: unknown;
navbar: ReactNode;
sidebar: ReactNode;
}) {

return (

<ApplicationLayoutShell sidebar={sidebar}>
<div className="">
<InternalNavbar>
<div className="flex w-full justify-between items-center">
<Suspense>{navbar}</Suspense>
</div>
</InternalNavbar>
<div className="m-6">
{children}
</div>
</div>
</ApplicationLayoutShell>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tables } from "@/lib/database.types";
import { AnimatePresence, motion } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
import React, { useState } from 'react';
import { statusColors } from "../../(specific-project-pages)/RunsTable";

export const RunDetails: React.FC<{ run: Tables<'digger_runs'> }> = ({ run }) => {
const [isExpanded, setIsExpanded] = useState(false);

return (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<Card className="mb-4">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Run {run.id}
</CardTitle>
<Badge className={statusColors[run.status]}>
{run.status.toUpperCase()}
</Badge>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center">
<div>
<p className="text-xs text-muted-foreground">Commit: {run.commit_id}</p>
<p className="text-xs text-muted-foreground">Date: {run.created_at}</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
>
<ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''
}`}
/>
</Button>
</div>
<AnimatePresence>
{isExpanded && run.status && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="mt-4"
>
<h4 className="text-sm font-semibold mb-2">Plan Summary</h4>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-center">
<span className="text-green-500 font-bold">{run.status}</span>
<span className="text-xs">Created</span>
</div>
<div className="flex flex-col items-center">
<span className="text-yellow-500 font-bold">{run.status}</span>
<span className="text-xs">Updated</span>
</div>
<div className="flex flex-col items-center">
<span className="text-red-500 font-bold">{run.status}</span>
<span className="text-xs">Deleted</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</CardContent>
</Card>
</motion.div>
);
};
Loading

0 comments on commit f41a40c

Please sign in to comment.