From 6a8ed06c012e3c3e1c498e3ed061e80ec6cb8850 Mon Sep 17 00:00:00 2001 From: psiddharthdesign <107192927+psiddharthdesign@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:50:44 +0530 Subject: [PATCH] feat/create project under organization --- .../@sidebar/OrganizationSidebar.tsx | 100 +++-- .../@sidebar/default.tsx | 101 +++-- .../(specific-organization-pages)/page.tsx | 2 +- .../projects/OrganizationProjectsTable.tsx | 131 +++--- .../create}/page.tsx | 0 .../projects/page.tsx | 13 +- .../@sidebar/ProjectSidebar.tsx | 100 +++-- .../(specific-project-pages)/layout.tsx | 1 + .../(specific-project-pages)/page.tsx | 73 ++-- .../users/GetLoginLinkDialog.tsx | 1 + .../onboarding/OnboardingFlow.tsx | 3 + .../onboarding/OrganizationCreation.tsx | 11 +- .../(authenticated-pages)/onboarding/page.tsx | 3 + src/components/CreateProjectDialog.tsx | 153 ------- src/components/CreateProjectForm.tsx | 387 +++++++++++------- src/components/InputTags.tsx | 72 ++++ .../OrganizationSwitcher.tsx | 2 +- src/components/SidebarLink.tsx | 15 +- src/data/user/organizations.ts | 25 +- src/data/user/projects.tsx | 24 +- src/data/user/repos.ts | 18 + src/lib/database.types.ts | 87 +++- .../20240718105020_add_new_tables.sql | 35 ++ .../20240718184709_add_user_projects.sql | 4 + 24 files changed, 778 insertions(+), 583 deletions(-) rename src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/{create-project => projects/create}/page.tsx (100%) delete mode 100644 src/components/CreateProjectDialog.tsx create mode 100644 src/components/InputTags.tsx create mode 100644 src/data/user/repos.ts create mode 100644 supabase/migrations/20240718105020_add_new_tables.sql create mode 100644 supabase/migrations/20240718184709_add_user_projects.sql diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx index 20af076c..baca2915 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx @@ -1,4 +1,3 @@ -import { ProFeatureGateDialog } from '@/components/ProFeatureGateDialog'; import { SidebarLink } from '@/components/SidebarLink'; import { fetchSlimOrganizations, getOrganizationSlugByOrganizationId } from '@/data/user/organizations'; import { cn } from '@/utils/cn'; @@ -8,9 +7,7 @@ import { Suspense } from 'react'; import { DesktopSidebarFallback } from '@/components/SidebarComponents/SidebarFallback'; import { SwitcherAndToggle } from '@/components/SidebarComponents/SidebarLogo'; -import { SubscriptionCardSmall } from '@/components/SubscriptionCardSmall'; -import { T } from '@/components/ui/Typography'; -import { DollarSign, FileBox, Home, Layers, Settings, UserRound } from 'lucide-react'; +import { Activity, FileText, GitCompare, Home, Layers, MessageCircle, Settings, Shield } from 'lucide-react'; async function OrganizationSidebarInternal({ organizationId, @@ -20,63 +17,60 @@ async function OrganizationSidebarInternal({ organizationSlug: string; }) { const slimOrganizations = await fetchSlimOrganizations(); + return (
-
-
- -
-
-
- } - /> - } - /> - } - /> - } - /> - } - /> - - } - /> - -
-
+
+
-
- Loading subscription details...}> -
- -
-
- +
+ } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + />
); diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/default.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/default.tsx index 136465de..246cea91 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/default.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/default.tsx @@ -1,16 +1,13 @@ -import { ProFeatureGateDialog } from '@/components/ProFeatureGateDialog'; import { SidebarLink } from '@/components/SidebarLink'; -import { SubscriptionCardSmall } from '@/components/SubscriptionCardSmall'; -import { T } from '@/components/ui/Typography'; -import { fetchSlimOrganizations, getOrganizationSlugByOrganizationId } from '@/data/user/organizations'; import { cn } from '@/utils/cn'; import { notFound } from 'next/navigation'; import { Suspense } from 'react'; import { DesktopSidebarFallback } from '@/components/SidebarComponents/SidebarFallback'; import { SwitcherAndToggle } from '@/components/SidebarComponents/SidebarLogo'; +import { fetchSlimOrganizations, getOrganizationSlugByOrganizationId } from '@/data/user/organizations'; import { organizationParamSchema } from '@/utils/zod-schemas/params'; -import { DollarSign, FileBox, Home, Layers, Settings, UserRound } from 'lucide-react'; +import { Activity, FileText, GitCompare, Home, Layers, MessageCircle, Settings, Shield } from 'lucide-react'; async function OrganizationSidebarInternal({ organizationId, @@ -24,58 +21,56 @@ async function OrganizationSidebarInternal({ return (
-
-
- -
-
-
- } - /> - } - /> - } - /> - } - /> - } - /> - - } - /> - -
- {/* */} -
-
-
- Loading subscription details...}> - - +
+ +
+
+ } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + />
); diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/page.tsx index 1b6800a9..334fe73d 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/page.tsx @@ -51,7 +51,7 @@ async function Dashboard({ params, searchParams }: DashboardProps) { Export PDF - + ), }, { - accessorKey: 'project_status', - header: 'Status', + accessorKey: 'latest_action_on', + header: 'Latest Action On', cell: ({ row }) => { - const status = row.getValue('project_status') as ProjectStatus - return ( - - {ProjectStatus[status]} - - ) + const date = moment(row.getValue('latest_action_on') as string); + return date.format('MMM DD YYYY, HH:mm [hrs]'); }, }, { - accessorKey: 'created_at', - cell: info => format(new Date(String(info.getValue())), 'dd MMMM yyyy') - , - header: ({ column }) => ( - + accessorKey: 'by', + header: 'By', + cell: ({ row }) => row.getValue('by') || 'unknown', + }, + { + accessorKey: 'terraform_working_dir', + header: 'Configuration Source / Working Directory', + cell: ({ row }) => ( +
+ + + {row.original.repoName ? `${row.original.repoName.toLowerCase().replace(/\s+/g, '-').replace(/[A-Z]/g, (letter) => letter.toLowerCase())}` : ''} + {row.original.repoName && ((row.getValue('terraform_working_dir') as string) || '') ? '/' : ''} + {((row.getValue('terraform_working_dir') as string) || '').replace(/\s+/g, '-').replace(/[A-Z]/g, (letter) => letter.toLowerCase())} + +
), - enableSorting: true, }, ]; const [sorting, setSorting] = useState([]); const table = useReactTable({ - data: projects, + data: projectsWithRepos, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), @@ -106,26 +103,30 @@ export function OrganizationProjectsTable({ projects }: Props) { }); if (projects.length === 0) { - - return -
- No projects found -
-
- + return ( + +
+ No projects found +
+
+ ); } return (
- -
+ {table.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map(header => ( - - {flexRender(header.column.columnDef.header, header.getContext())} + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} ))} @@ -133,9 +134,9 @@ export function OrganizationProjectsTable({ projects }: Props) { {table.getRowModel().rows.map(row => ( - + {row.getVisibleCells().map(cell => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -143,7 +144,7 @@ export function OrganizationProjectsTable({ projects }: Props) { ))}
-
-
+ +
); -} +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/create-project/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/create/page.tsx similarity index 100% rename from src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/create-project/page.tsx rename to src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/create/page.tsx diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx index 43e1d557..891ad52c 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/projects/page.tsx @@ -1,14 +1,16 @@ -import { CreateProjectDialog } from "@/components/CreateProjectDialog"; import { PageHeading } from "@/components/PageHeading"; import { Pagination } from "@/components/Pagination"; import { Search } from "@/components/Search"; import { T } from "@/components/ui/Typography"; +import { Button } from "@/components/ui/button"; import { getProjects, getProjectsTotalCount } from "@/data/user/projects"; import { organizationParamSchema, projectsfilterSchema } from "@/utils/zod-schemas/params"; +import { Plus } from "lucide-react"; import type { Metadata } from "next"; +import Link from "next/link"; import { Suspense } from "react"; import type { DashboardProps } from "../page"; import { OrganizationProjectsTable } from "./OrganizationProjectsTable"; @@ -44,7 +46,7 @@ export default async function Page({ const filters = projectsfilterSchema.parse(searchParams); return ( -
+
- + + +
{ -
+
-
- } - /> - {project.team_id && ( - } - /> - )} - } - /> - } - /> - } - /> - } - /> - } - /> -
- + +
+ } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> +
); } @@ -82,4 +80,4 @@ export async function ProjectSidebar({ params }: { params: unknown }) {
); -} +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/layout.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/layout.tsx index 835fda44..314b6179 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/layout.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/layout.tsx @@ -36,6 +36,7 @@ export default async function ProjectLayout({ const project = await getSlimProjectBySlug(projectSlug); return ( +
diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/page.tsx index 5286ba5f..1ea43755 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/page.tsx @@ -1,12 +1,9 @@ -import { ChatContainer } from "@/components/chat-container"; -import { T } from "@/components/ui/Typography"; -import { getSlimProjectBySlug } from "@/data/user/projects"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getProjectById, getSlimProjectBySlug } from "@/data/user/projects"; import { projectSlugParamSchema } from "@/utils/zod-schemas/params"; -import { nanoid } from "ai"; +import { CalendarIcon, GitBranchIcon, LayersIcon } from "lucide-react"; import type { Metadata } from "next"; -import { Suspense } from "react"; -import { CommentInput } from "./CommentInput"; -import { ProjectComments } from "./ProjectComments"; type ProjectPageProps = { params: { @@ -20,7 +17,6 @@ export async function generateMetadata({ const { projectSlug } = projectSlugParamSchema.parse(params); const project = await getSlimProjectBySlug(projectSlug); - return { title: `Project | ${project.name}`, description: `View and manage your project ${project.name}`, @@ -29,32 +25,51 @@ export async function generateMetadata({ export default async function ProjectPage({ params }: { params: unknown }) { const { projectSlug } = projectSlugParamSchema.parse(params); - const project = await getSlimProjectBySlug(projectSlug); + const slimProject = await getSlimProjectBySlug(projectSlug); + const project = await getProjectById(slimProject.id); - const newChatId = nanoid(); return (
-
-
-
- + + +
+ {project.name} + {project.project_status}
- -
-
- Comments -
-
- - - - + + +
+
+ + Created: {new Date(project.created_at).toLocaleDateString()} +
+
+ + Organization: {project.organization_id} +
+
+ + Repository: {project.repo_id}
-
-
+ + + + + + Project Details + + +
+

Terraform Working Directory: {project.terraform_working_dir}

+

Managing State: {project.is_managing_state ? 'Yes' : 'No'}

+

In Main Branch: {project.is_in_main_branch ? 'Yes' : 'No'}

+

Generated: {project.is_generated ? 'Yes' : 'No'}

+

Latest Action: {project.latest_action_on ? new Date(project.latest_action_on).toLocaleString() : 'No actions'}

+
+
+
+
); -} +} \ No newline at end of file diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/app_admin/(admin-pages)/users/GetLoginLinkDialog.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/app_admin/(admin-pages)/users/GetLoginLinkDialog.tsx index 899d9ce2..2b373f68 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/app_admin/(admin-pages)/users/GetLoginLinkDialog.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/app_admin/(admin-pages)/users/GetLoginLinkDialog.tsx @@ -1,3 +1,4 @@ + 'use client'; import { Button } from '@/components/ui/button'; import { diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx index 705e368f..369c6baf 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx @@ -32,6 +32,7 @@ export function UserOnboardingFlow({ const [currentStep, setCurrentStep] = useState( getInitialFlowState(flowStates, onboardingStatus) ); + const { replace } = useRouter(); const nextStep = useCallback(() => { @@ -53,6 +54,8 @@ export function UserOnboardingFlow({ exit: { opacity: 0, y: -50 }, }; + console.log(currentStep) + return ( - createOrganization(organizationTitle, organizationSlug, { isOnboardingFlow: true }), - onSuccess: () => { + mutationFn: async ({ organizationTitle, organizationSlug }: CreateOrganizationSchema) => { + console.log('creating organization mutation'); + return createOrganization(organizationTitle, organizationSlug, { isOnboardingFlow: true }) + } + , + onSuccess: (data) => { + console.log('success', data); toast({ title: "Organization created!", description: "Your new organization is ready." }); onSuccess(); }, @@ -36,6 +40,7 @@ export function OrganizationCreation({ onSuccess }: OrganizationCreationProps) { }); const onSubmit = (data: CreateOrganizationSchema) => { + console.log('submitting'); createOrgMutation.mutate(data); }; diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx index e89e21e5..45077bbb 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx @@ -47,7 +47,10 @@ async function OnboardingFlowWrapper({ userId, userEmail }: { userId: string; us serverGetLoggedInUser(), ]); const { userProfile } = onboardingConditions; + console.log(userProfile); const onboardingStatus = authUserMetadataSchema.parse(user.user_metadata); + console.log(onboardingStatus); + console.log(userEmail); return ( - await createProjectAction({ organizationId, name, slug }), - { - loadingMessage: "Creating project...", - successMessage: "Project created!", - errorMessage: "Failed to create project", - onSuccess: (response) => { - setOpen(false); - if (response.status === "success" && response.data) { - router.push(`/project/${response.data.slug}`); - } - }, - }, - ); - - const onSubmit: SubmitHandler<{ name: string; slug: string }> = (data) => { - createProjectMutation.mutate({ - organizationId, - name: data.name, - slug: data.slug, - }); - }; - - return ( - <> - - - - - - -
- -
-
- Create Project - - Create a new project and get started. - -
-
-
-
- - { - setValue("name", e.target.value); - setValue("slug", generateSlug(e.target.value), { - shouldValidate: true, - }); - }} - id="name" - type="text" - placeholder="Project Name" - disabled={createProjectMutation.isLoading} - /> -
-
- - -
- - - - - -
-
-
- - ); -} diff --git a/src/components/CreateProjectForm.tsx b/src/components/CreateProjectForm.tsx index ef2c4422..02ad532a 100644 --- a/src/components/CreateProjectForm.tsx +++ b/src/components/CreateProjectForm.tsx @@ -3,166 +3,269 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { createProjectAction } from "@/data/user/projects"; +import { useSAToastMutation } from "@/hooks/useSAToastMutation"; +import { generateSlug } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; import { motion } from "framer-motion"; -import { Check, Github, MonitorOff, Play, RotateCw } from "lucide-react"; +import { Check, Github } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useState } from 'react'; +import { Controller, useForm } from "react-hook-form"; +import { z } from "zod"; +import { InputTags } from "./InputTags"; import { T } from "./ui/Typography"; const MotionCard = motion(Card); +const createProjectFormSchema = z.object({ + name: z.string().min(1, "Project name is required"), + repository: z.number().int().positive("Repository ID must be a positive integer"), + terraformDir: z.string().min(1, "Terraform working directory is required"), + managedState: z.boolean(), + labels: z.array(z.string()), +}); + +type CreateProjectFormData = z.infer; + +const repositories = [ + { id: 12, name: 'Repository 1' }, + { id: 123, name: 'Repository 2' }, + { id: 41, name: 'Repository 3' }, + { id: 24, name: 'Repository 4' }, + { id: 124, name: 'Repository 5' }, +]; + export default function CreateProjectForm({ organizationId }: { organizationId: string }) { - const [selectedRepo, setSelectedRepo] = useState(''); - const [autoQueueRun, setAutoQueueRun] = useState('apply-before-merge'); + const [selectedRepo, setSelectedRepo] = useState(repositories[0].id); + const router = useRouter(); + + const { control, handleSubmit, setValue, watch } = useForm({ + resolver: zodResolver(createProjectFormSchema), + defaultValues: { + name: "", + repository: repositories[0].id, + terraformDir: "", + managedState: true, + labels: [], + }, + }); + + const createProjectMutation = useSAToastMutation( + async (data: CreateProjectFormData) => { + const slug = generateSlug(data.name); + return await createProjectAction({ + organizationId, + name: data.name, + slug, + repoId: data.repository, + terraformWorkingDir: data.terraformDir, + isManagingState: data.managedState, + labels: data.labels, + }); + }, + { + loadingMessage: "Creating project...", + successMessage: "Project created!", + errorMessage: "Failed to create project", + onSuccess: (response) => { + if (response.status === "success" && response.data) { + router.push(`/project/${response.data.slug}`); + } + }, + }, + ); + + const onSubmit = (data: CreateProjectFormData) => { + createProjectMutation.mutate(data); + }; + - const repositories = [ - { id: 'repo1', name: 'Repository 1' }, - { id: 'repo2', name: 'Repository 2' }, - { id: 'repo3', name: 'Repository 3' }, - { id: 'repo4', name: 'Repository 4' }, - { id: 'repo5', name: 'Repository 5' }, - ]; return (
-
-
- Create new Project - Create a new project within your organization. -
-
- - -
-
- - - -
- Project Details - Provide the name of your project -
-
- -
-
- - -
-
-
-
- - - -
- Select a repository - Choose the repository for your project -
- - - Connected to GitHub - - -
- - -
- {repositories.map((repo, index) => ( - setSelectedRepo(repo.id)} - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: index * 0.1 }} - > - - - {repo.name} - - - ))} -
- -
-
-
- - - -
- Terraform Configuration - Specify the working directory for Terraform +
{ + e.preventDefault(); + handleSubmit(onSubmit)(e); + }}> + {/* {Object.entries(watch()).map(([key, value]) => ( +
+ {key}: {JSON.stringify(value)}
- - + ))} */} +
- - + Create new Project + Create a new project within your organization.
- - - - - -
- Auto Queue Runs - Configure when to automatically queue runs +
+ +
- - -
- {[ - { id: 'apply-before-merge', label: 'Apply Before Merge', icon: Play }, - { id: 'apply-after-merge', label: 'Apply After Merge', icon: RotateCw }, - { id: 'never', label: 'Never', icon: MonitorOff }, - ].map((option, index) => ( - setAutoQueueRun(option.id)} - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: index * 0.1 }} - > - - - {option.label} -

- Provide information regarding your project. -

-
-
- ))} -
-
- +
+ + + +
+ Project Details + Provide the name of your project +
+
+ +
+ + ( + + )} + /> +
+
+
+ + + +
+ Select a repository + Choose the repository for your project +
+ + + Connected to GitHub + + +
+ + +
+ {repositories.map((repo, index) => ( + { + setSelectedRepo(repo.id); + setValue("repository", repo.id); + }} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: index * 0.1 }} + > + + + {repo.name} + + + ))} +
+ +
+
+
+ + + +
+ Terraform Configuration + Specify the working directory for Terraform +
+
+ +
+ + ( + + )} + /> +
+
+
+ + + +
+ Additional Settings + Configure additional project settings +
+
+ +
+
+ ( + + )} + /> + +
+
+ + ( + + )} + /> +
+
+
+
+
); } \ No newline at end of file diff --git a/src/components/InputTags.tsx b/src/components/InputTags.tsx new file mode 100644 index 00000000..4146e62e --- /dev/null +++ b/src/components/InputTags.tsx @@ -0,0 +1,72 @@ +'use client' + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input, InputProps } from "@/components/ui/input"; +import { XIcon } from "lucide-react"; +import { Dispatch, SetStateAction, forwardRef, useState } from "react"; + +type InputTagsProps = InputProps & { + value: string[]; + onChange: Dispatch>; +}; + +export const InputTags = forwardRef( + ({ value, onChange, ...props }, ref) => { + const [pendingDataPoint, setPendingDataPoint] = useState(""); + + const addPendingDataPoint = () => { + if (pendingDataPoint.trim()) { + const newDataPoints = new Set([...value, pendingDataPoint.trim()]); + onChange(Array.from(newDataPoints)); + setPendingDataPoint(""); + } + }; + + return ( + <> +
+ setPendingDataPoint(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + addPendingDataPoint(); + } + }} + className="rounded-r-none" + {...props} + ref={ref} + /> + +
+ {value.length > 0 && ( +
+ {value.map((item, idx) => ( + + {item} + + + ))} +
+ )} + + ); + } +); \ No newline at end of file diff --git a/src/components/SidebarComponents/OrganizationSwitcher.tsx b/src/components/SidebarComponents/OrganizationSwitcher.tsx index 8beb58b1..780de508 100644 --- a/src/components/SidebarComponents/OrganizationSwitcher.tsx +++ b/src/components/SidebarComponents/OrganizationSwitcher.tsx @@ -75,7 +75,7 @@ export function OrganizationSwitcher({ key={organization.id} onSelect={() => { setIsPopoverOpen(false); - router.push(`/${organization.slug}`); + router.push(`/org/${organization.id}`); }} className="text-sm flex items-start" > diff --git a/src/components/SidebarLink.tsx b/src/components/SidebarLink.tsx index 0066d34b..d7c3018e 100644 --- a/src/components/SidebarLink.tsx +++ b/src/components/SidebarLink.tsx @@ -4,6 +4,7 @@ import { MOBILE_MEDIA_QUERY_MATCHER } from '@/constants'; import { SidebarVisibilityContext } from '@/contexts/SidebarVisibilityContext'; import useMatchMedia from '@/hooks/useMatchMedia'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import React, { useContext } from 'react'; type SidebarLinkProps = { @@ -15,16 +16,24 @@ type SidebarLinkProps = { export function SidebarLink({ label, href, icon }: SidebarLinkProps) { const { setVisibility } = useContext(SidebarVisibilityContext); const isMobile = useMatchMedia(MOBILE_MEDIA_QUERY_MATCHER); + const pathname = usePathname(); + const isActive = pathname === href; return (
-
{icon}
+
+ {icon} +
isMobile && setVisibility(false)} - className="p-2 w-full text-sm group-hover:text-gray-800 dark:group-hover:text-slate-300" + className={`p-2 w-full text-sm ${isActive ? 'text-foreground' : 'text-foreground group-hover:text-foreground' + }`} href={href} > {label} diff --git a/src/data/user/organizations.ts b/src/data/user/organizations.ts index f53bb934..0bd3477e 100644 --- a/src/data/user/organizations.ts +++ b/src/data/user/organizations.ts @@ -102,33 +102,12 @@ export const createOrganization = async ( if (isOnboardingFlow) { // insert 3 dummy projects - - const { error: projectError } = await supabaseClient - .from('projects') - .insert([ - { - organization_id: organizationId, - name: 'Project 1', - }, - { - organization_id: organizationId, - name: 'Project 2', - }, - { - organization_id: organizationId, - name: 'Project 3', - }, - ]); - - if (projectError) { - return { status: 'error', message: projectError.message }; - } - const { error: updateError } = await supabaseClient .from('user_private_info') .update({ default_organization: organizationId }) .eq('id', user.id); + console.log('updateError', updateError); if (updateError) { return { status: 'error', message: updateError.message }; } @@ -152,6 +131,8 @@ export const createOrganization = async ( if (refreshSessionResponse.status === 'error') { return refreshSessionResponse; } + + throw new Error('failed'); } return { diff --git a/src/data/user/projects.tsx b/src/data/user/projects.tsx index 44351c1e..02055915 100644 --- a/src/data/user/projects.tsx +++ b/src/data/user/projects.tsx @@ -66,10 +66,18 @@ export const createProjectAction = async ({ organizationId, name, slug, + repoId, + terraformWorkingDir, + isManagingState, + labels, }: { organizationId: string; name: string; slug: string; + repoId: number; + terraformWorkingDir: string; + isManagingState: boolean; + labels: string[]; }): Promise>> => { "use server"; const supabaseClient = createSupabaseUserServerActionClient(); @@ -79,12 +87,24 @@ export const createProjectAction = async ({ organization_id: organizationId, name, slug, + repo_id: repoId, + terraform_working_dir: terraformWorkingDir, + is_managing_state: isManagingState, + is_in_main_branch: true, + is_generated: true, + project_status: "draft", + latest_action_on: new Date().toISOString(), + labels, + }) .select("*") .single(); + console.log('createProjectAction', project); + if (error) { + console.log('createProjectAction', error); return { status: 'error', message: error.message, @@ -92,8 +112,8 @@ export const createProjectAction = async ({ } - revalidatePath("/[organizationSlug]", "layout"); - revalidatePath("/[organizationSlug]/projects", "layout"); + revalidatePath(`/org/[organizationId]`, "layout"); + revalidatePath(`/org/[organizationId]/projects/`, "layout"); return { status: 'success', diff --git a/src/data/user/repos.ts b/src/data/user/repos.ts new file mode 100644 index 00000000..6e4ebcb9 --- /dev/null +++ b/src/data/user/repos.ts @@ -0,0 +1,18 @@ +'use server'; + +import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; + +export async function getRepoDetails(repoId: number) { + const supabaseClient = createSupabaseUserServerComponentClient(); + const { data, error } = await supabaseClient + .from('repos') + .select('id, name') + .eq('id', repoId) + .single(); + + if (error) { + throw error; + } + + return data; +} diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts index e86b6f49..1ba527e4 100644 --- a/src/lib/database.types.ts +++ b/src/lib/database.types.ts @@ -2,7 +2,7 @@ export type Json = | string | number | boolean - | null + | null | { [key: string]: Json | undefined } | Json[] @@ -681,36 +681,119 @@ export type Database = { } projects: { Row: { + configuration_yaml: string | null created_at: string + deleted_at: string | null id: string + is_generated: boolean | null + is_in_main_branch: boolean | null + is_managing_state: boolean | null + labels: string[] | null + latest_action_on: string | null name: string organization_id: string project_status: Database["public"]["Enums"]["project_status"] + repo_id: number slug: string + status: string | null team_id: number | null + terraform_working_dir: string | null updated_at: string } Insert: { + configuration_yaml?: string | null created_at?: string + deleted_at?: string | null id?: string + is_generated?: boolean | null + is_in_main_branch?: boolean | null + is_managing_state?: boolean | null + labels?: string[] | null + latest_action_on?: string | null name: string organization_id: string project_status?: Database["public"]["Enums"]["project_status"] + repo_id?: number slug?: string + status?: string | null team_id?: number | null + terraform_working_dir?: string | null updated_at?: string } Update: { + configuration_yaml?: string | null created_at?: string + deleted_at?: string | null id?: string + is_generated?: boolean | null + is_in_main_branch?: boolean | null + is_managing_state?: boolean | null + labels?: string[] | null + latest_action_on?: string | null name?: string organization_id?: string project_status?: Database["public"]["Enums"]["project_status"] + repo_id?: number slug?: string + status?: string | null team_id?: number | null + terraform_working_dir?: string | null updated_at?: string } - Relationships: [] + Relationships: [ + { + foreignKeyName: "fk_projects_organization" + columns: ["organization_id"] + isOneToOne: false + referencedRelation: "organizations" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_projects_repo" + columns: ["repo_id"] + isOneToOne: false + referencedRelation: "repos" + referencedColumns: ["id"] + }, + ] + } + repos: { + Row: { + created_at: string | null + deleted_at: string | null + digger_config: string | null + id: number + name: string + organization_id: string | null + updated_at: string | null + } + Insert: { + created_at?: string | null + deleted_at?: string | null + digger_config?: string | null + id?: number + name: string + organization_id?: string | null + updated_at?: string | null + } + Update: { + created_at?: string | null + deleted_at?: string | null + digger_config?: string | null + id?: number + name?: string + organization_id?: string | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "fk_repos_organization" + columns: ["organization_id"] + isOneToOne: false + referencedRelation: "organizations" + referencedColumns: ["id"] + }, + ] } subscriptions: { Row: { diff --git a/supabase/migrations/20240718105020_add_new_tables.sql b/supabase/migrations/20240718105020_add_new_tables.sql new file mode 100644 index 00000000..daceb0f1 --- /dev/null +++ b/supabase/migrations/20240718105020_add_new_tables.sql @@ -0,0 +1,35 @@ +CREATE TABLE "public"."repos" ( + "id" bigserial NOT NULL, + "created_at" timestamptz DEFAULT now(), + "updated_at" timestamptz, + "deleted_at" timestamptz, + "name" text, + "organization_id" uuid, + "digger_config" text, + PRIMARY KEY ("id"), + CONSTRAINT "fk_repos_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION +); +-- Create index "idx_org_repo" to table: "repos" +CREATE UNIQUE INDEX "idx_org_repo" ON "public"."repos" ("name", "organization_id"); +-- Create index "idx_repos_deleted_at" to table: "repos" +CREATE INDEX "idx_repos_deleted_at" ON "public"."repos" ("deleted_at"); +-- Create "projects" table + +ALTER TABLE "public"."projects" + ADD COLUMN "latest_action_on" text, + ADD COLUMN "repo_id" bigserial, + ADD COLUMN "configuration_yaml" text, + ADD COLUMN "status" text, + ADD COLUMN "is_generated" boolean, + ADD COLUMN "is_in_main_branch" boolean, + ADD COLUMN "deleted_at" timestamptz, + ADD COLUMN "terraform_working_dir" text, + ADD COLUMN "is_managing_state" boolean, + ADD COLUMN "labels" text[]; + + +ALTER TABLE "public"."projects" + ADD CONSTRAINT "fk_projects_organization" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION; + +ALTER TABLE "public"."projects" + ADD CONSTRAINT "fk_projects_repo" FOREIGN KEY ("repo_id") REFERENCES "public"."repos" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION; \ No newline at end of file diff --git a/supabase/migrations/20240718184709_add_user_projects.sql b/supabase/migrations/20240718184709_add_user_projects.sql new file mode 100644 index 00000000..7ea9d500 --- /dev/null +++ b/supabase/migrations/20240718184709_add_user_projects.sql @@ -0,0 +1,4 @@ +-- Add NOT NULL constraint to the name column in the repos table +ALTER TABLE "public"."repos" +ALTER COLUMN "name" SET NOT NULL; +