Skip to content

Commit

Permalink
Add projects list and new project form
Browse files Browse the repository at this point in the history
  • Loading branch information
mkleszcz committed May 7, 2023
1 parent 47950e8 commit 8f80dc5
Show file tree
Hide file tree
Showing 30 changed files with 708 additions and 55 deletions.
3 changes: 2 additions & 1 deletion apps/app/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"importOrder": [
"^(path|dns|fs)/?",
"<THIRD_PARTY_MODULES>",
"@vm|@app",
"@vm",
"@app",
"^(__generated__|__generated|@types|app|contexts|emails|fonts|images|mocks|modules|routes|shared|tests|theme|translations)/?",
"^[./]"
],
Expand Down
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"tailwindcss": "3.3.2",
"typescript": "5.0.4"
},
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/components/dashboard/forms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './newProjectForm';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './newProjectForm.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useMutation } from '@apollo/client';
import { PlusIcon } from '@heroicons/react/24/solid';
import { useForm } from 'react-hook-form';

import { ValidationError } from '@vm/api-client/__generated/gql/graphql';

import { newProjectMutation } from '@app/graphql/projects.graphql';

type NewProjectFormData = {
name: string;
};

export const NewProjectForm = () => {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<NewProjectFormData>();

const [createProject, { loading, error }] = useMutation(newProjectMutation);

const onSubmit = async (formData: NewProjectFormData) => {
try {
const response = await createProject({
variables: {
name: formData.name,
},
});

const responseData = response.data?.createProject;

if (response.data) {
console.log(response.data);
// handle success
reset();
} else {
const { message, fieldErrors } = responseData as ValidationError;
// handle validation error
console.error(message, fieldErrors);
}
} catch (error) {
// handle other errors
console.error(error);
}
};

return (
<form onSubmit={handleSubmit(onSubmit)} className="flex items-center mb-4">
<input
type="text"
{...register('name', { required: true })}
className="flex-grow p-2 mr-2 border border-gray-400 rounded-md bg-white text-gray-800 dark:bg-gray-800 dark:text-white"
placeholder="Enter project name"
/>
<button
type="submit"
className={`flex items-center px-4 py-2 rounded-md ${
loading ? 'bg-gray-400 cursor-wait' : 'bg-blue-500 hover:bg-blue-600'
}`}
disabled={loading}
>
<PlusIcon className="w-5 h-5 mr-2" />
Add Project
</button>
{errors.name && (
<p className="ml-2 text-red-500">
{errors.name.type === 'required' ? 'Project name is required' : errors.name.message}
</p>
)}
{error && (
<p className="ml-2 text-red-500">
{error.message.includes('validation') ? error.message : 'Something went wrong'}
</p>
)}
</form>
);
};
1 change: 1 addition & 0 deletions apps/app/src/components/dashboard/navigation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Navigation } from './navigation.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from 'next/link';

export const Navigation = () => {
return (
<div className="flex flex-col flex-auto">
<div className="grow p-4 px-2">
<div className="grid gap-y-1">
<Link href="/dashboard" className="p-2 rounded-lg hover:bg-indigo-900">
Dashboard
</Link>
<Link href="/projects" className="p-2 rounded-lg hover:bg-indigo-900">
Projects
</Link>
</div>
</div>
<div className="flex-none justify-self-end p-4">
<a href="#">Logout</a>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useQuery } from '@apollo/client';
import { PlusIcon } from '@heroicons/react/24/solid';
import Link from 'next/link';
import React from 'react';

import { ProjectsListItem } from '@app/components/dashboard/projectsList/projectsListItem.component';
import { projectsQuery } from '@app/graphql';

export const ProjectsList = () => {
const { loading, data } = useQuery(projectsQuery);

if (loading || !data) {
return null;
}

return (
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-bold">Projects</h1>
<Link
href="/projects/new"
className="flex items-center px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600"
>
<PlusIcon className="w-5 h-5 mr-2" />
New Project
</Link>
</div>
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{data?.projects.map((project) => (
<ProjectsListItem project={project} key={project.id} />
))}
</ul>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Menu, Transition } from '@headlessui/react';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/solid';
import Link from 'next/link';
import React from 'react';

import { FragmentType, getFragmentData } from '@vm/api-client/__generated/gql';

import { projectFragment } from '@app/graphql';

interface ProjectsListItemProps {
project: FragmentType<typeof projectFragment>;
}

export const ProjectsListItem = ({ project }: ProjectsListItemProps) => {
const data = getFragmentData(projectFragment, project);
return (
<li className="flex flex-col p-4 bg-white dark:bg-slate-900 rounded-md shadow-md">
<h2 className="mb-2 text-xl font-bold">{data.name}</h2>
<div className="flex mb-2 space-x-2">
<Link
href={`/projects/${data.id}/edit`}
className="flex items-center px-2 py-1 text-white bg-green-500 rounded-md hover:bg-green-600"
>
<PencilIcon className="w-5 h-5 mr-2" />
Edit
</Link>
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="flex items-center px-2 py-1 text-white bg-red-500 rounded-md hover:bg-red-600">
<TrashIcon className="w-5 h-5 mr-2" />
Delete
</Menu.Button>
</div>
<Transition
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 w-48 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
className={`${active ? 'bg-gray-100' : ''} block w-full text-left px-4 py-2 text-sm`}
onClick={() => {
// Handle delete
}}
>
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
<p className="text-sm text-gray-600 dark:text-slate-400">{data.environments.edges.length} environments</p>
</li>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Inter } from 'next/font/google';
import { PropsWithChildren, ReactNode } from 'react';

import { Navigation } from '@app/components/dashboard/navigation';

const inter = Inter({ subsets: ['latin'] });

export const DashboardLayout = ({ children }: PropsWithChildren) => {
return (
<div className={`flex flex-row min-h-screen ${inter.className}`}>
<div className="basis-60 bg-indigo-800 flex">
<Navigation />
</div>
<div className="grow flex flex-col bg-white dark:bg-slate-900">
<header className="bg-white shadow dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8"></div>
</header>
<div className="grow overflow-auto bg-white dark:bg-gray-700">{children}</div>
</div>
</div>
);
};

export const getDashboardLayout = (page: ReactNode) => <DashboardLayout>{page}</DashboardLayout>;
1 change: 1 addition & 0 deletions apps/app/src/components/layouts/dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dashboardLayout.component';
1 change: 1 addition & 0 deletions apps/app/src/components/layouts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dashboard';
1 change: 1 addition & 0 deletions apps/app/src/components/projectVersionsView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProjectVersionsView } from './projectVersionsView.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useQuery } from '@apollo/client';
import React from 'react';

import { projectsQuery } from './projectVersionsView.graphql';

interface ProjectVersionsViewProps {
projectId: string;
}

export const ProjectVersionsView = ({ projectId }: ProjectVersionsViewProps) => {
const { loading, data } = useQuery(projectsQuery, { variables: { id: projectId } });

if (loading || !data) {
return null;
}

const project = data.project;
return (
<>
<div className="text-xl p-2">{project.name}</div>
<div className="grid grid-flow-col justify-stretch">
{project.environments.edges.map((environmentNode, key) => {
const env = environmentNode?.node;
return (
<div
className="bg-white dark:bg-slate-800 rounded-lg overflow-hidden m-2 ring-1 ring-slate-900/5 shadow-xl"
key={key}
>
<div className="p-2">
<div className="text-slate-500 dark:text-slate-400">Environment name</div>
<div className="text-xl font-medium text-slate-950 dark:text-white">{env?.name}</div>
<a href="#" className="text-blue-600">
Open environment
</a>
</div>
<div className="p-2">
<div className="text-slate-500 dark:text-slate-400">Version</div>
<div className="text-xl font-medium text-green-800 dark:text-green-500">1.0.{key - 1}</div>
<div className="text-slate-400 text-sx">Updated: 6.05.23 16:41:11</div>
</div>
<div className="bg-gray-100 dark:bg-slate-700 p-2">
<a href="#" className="text-gray-500 dark:text-slate-400 font-medium">
Show environment history
</a>
</div>
</div>
);
})}
</div>

<div className="grid grid-flow-col justify-stretch">
{project.environments.edges.map((environmentNode, key) => {
const env = environmentNode?.node;
return (
<div className="bg-white dark:bg-slate-800 rounded-lg overflow-hidden m-2" key={key}>
<div className="p-2">
<div className="text-slate-500 dark:text-slate-400 font-medium">Environment services</div>
</div>
{project.services.edges.map((serviceNode, key) => {
const service = serviceNode?.node;
return (
<div className="p-2" key={key}>
<div className="text-slate-500 dark:text-slate-400 font-medium">{service?.name}</div>
<div className="text-xl font-medium text-green-800 dark:text-green-500"></div>
<div className="text-slate-400 text-sx">Updated: 6.05.23 16:41:11</div>
</div>
);
})}
</div>
);
})}
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { gql } from '@vm/api-client/__generated/gql';

export const projectsQuery = gql(/* GraphQL */ `
query projectQuery($id: ID!) {
project(id: $id) {
id
name
environments {
edges {
node {
id
name
currentVersion {
name
id
createdAt
}
}
}
}
services {
edges {
node {
name
}
}
}
}
}
`);
20 changes: 0 additions & 20 deletions apps/app/src/components/projects/projects.component.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions apps/app/src/components/projects/projects.graphql.ts

This file was deleted.

1 change: 1 addition & 0 deletions apps/app/src/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './projects.graphql';
Loading

0 comments on commit 8f80dc5

Please sign in to comment.