diff --git a/apps/app/.prettierrc b/apps/app/.prettierrc index 4bd449d..ee32dbb 100644 --- a/apps/app/.prettierrc +++ b/apps/app/.prettierrc @@ -6,7 +6,8 @@ "importOrder": [ "^(path|dns|fs)/?", "", - "@vm|@app", + "@vm", + "@app", "^(__generated__|__generated|@types|app|contexts|emails|fonts|images|mocks|modules|routes|shared|tests|theme|translations)/?", "^[./]" ], diff --git a/apps/app/package.json b/apps/app/package.json index cfd3481..39b00b2 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -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" }, diff --git a/apps/app/src/components/dashboard/forms/index.ts b/apps/app/src/components/dashboard/forms/index.ts new file mode 100644 index 0000000..5c451b5 --- /dev/null +++ b/apps/app/src/components/dashboard/forms/index.ts @@ -0,0 +1 @@ +export * from './newProjectForm'; diff --git a/apps/app/src/components/dashboard/forms/newProjectForm/index.ts b/apps/app/src/components/dashboard/forms/newProjectForm/index.ts new file mode 100644 index 0000000..3fae4e8 --- /dev/null +++ b/apps/app/src/components/dashboard/forms/newProjectForm/index.ts @@ -0,0 +1 @@ +export * from './newProjectForm.component'; diff --git a/apps/app/src/components/dashboard/forms/newProjectForm/newProjectForm.component.tsx b/apps/app/src/components/dashboard/forms/newProjectForm/newProjectForm.component.tsx new file mode 100644 index 0000000..faceb50 --- /dev/null +++ b/apps/app/src/components/dashboard/forms/newProjectForm/newProjectForm.component.tsx @@ -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(); + + 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 ( +
+ + + {errors.name && ( +

+ {errors.name.type === 'required' ? 'Project name is required' : errors.name.message} +

+ )} + {error && ( +

+ {error.message.includes('validation') ? error.message : 'Something went wrong'} +

+ )} +
+ ); +}; diff --git a/apps/app/src/components/dashboard/navigation/index.ts b/apps/app/src/components/dashboard/navigation/index.ts new file mode 100644 index 0000000..b7bc2f1 --- /dev/null +++ b/apps/app/src/components/dashboard/navigation/index.ts @@ -0,0 +1 @@ +export { Navigation } from './navigation.component'; diff --git a/apps/app/src/components/dashboard/navigation/navigation.component.tsx b/apps/app/src/components/dashboard/navigation/navigation.component.tsx new file mode 100644 index 0000000..f0c18fd --- /dev/null +++ b/apps/app/src/components/dashboard/navigation/navigation.component.tsx @@ -0,0 +1,21 @@ +import Link from 'next/link'; + +export const Navigation = () => { + return ( +
+
+
+ + Dashboard + + + Projects + +
+
+
+ Logout +
+
+ ); +}; diff --git a/apps/app/src/components/dashboard/projectsList/projectsList.component.tsx b/apps/app/src/components/dashboard/projectsList/projectsList.component.tsx new file mode 100644 index 0000000..8bcd0db --- /dev/null +++ b/apps/app/src/components/dashboard/projectsList/projectsList.component.tsx @@ -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 ( +
+
+

Projects

+ + + New Project + +
+
    + {data?.projects.map((project) => ( + + ))} +
+
+ ); +}; diff --git a/apps/app/src/components/dashboard/projectsList/projectsListItem.component.tsx b/apps/app/src/components/dashboard/projectsList/projectsListItem.component.tsx new file mode 100644 index 0000000..3fd7626 --- /dev/null +++ b/apps/app/src/components/dashboard/projectsList/projectsListItem.component.tsx @@ -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; +} + +export const ProjectsListItem = ({ project }: ProjectsListItemProps) => { + const data = getFragmentData(projectFragment, project); + return ( +
  • +

    {data.name}

    +
    + + + Edit + + +
    + + + Delete + +
    + + + + {({ active }) => ( + + )} + + + +
    +
    +

    {data.environments.edges.length} environments

    +
  • + ); +}; diff --git a/apps/app/src/components/layouts/dashboard/dashboardLayout.component.tsx b/apps/app/src/components/layouts/dashboard/dashboardLayout.component.tsx new file mode 100644 index 0000000..c6a1115 --- /dev/null +++ b/apps/app/src/components/layouts/dashboard/dashboardLayout.component.tsx @@ -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 ( +
    +
    + +
    +
    +
    +
    +
    +
    {children}
    +
    +
    + ); +}; + +export const getDashboardLayout = (page: ReactNode) => {page}; diff --git a/apps/app/src/components/layouts/dashboard/index.ts b/apps/app/src/components/layouts/dashboard/index.ts new file mode 100644 index 0000000..c166747 --- /dev/null +++ b/apps/app/src/components/layouts/dashboard/index.ts @@ -0,0 +1 @@ +export * from './dashboardLayout.component'; diff --git a/apps/app/src/components/layouts/index.ts b/apps/app/src/components/layouts/index.ts new file mode 100644 index 0000000..b58b6c9 --- /dev/null +++ b/apps/app/src/components/layouts/index.ts @@ -0,0 +1 @@ +export * from './dashboard'; diff --git a/apps/app/src/components/projectVersionsView/index.ts b/apps/app/src/components/projectVersionsView/index.ts new file mode 100644 index 0000000..8352fe6 --- /dev/null +++ b/apps/app/src/components/projectVersionsView/index.ts @@ -0,0 +1 @@ +export { ProjectVersionsView } from './projectVersionsView.component'; diff --git a/apps/app/src/components/projectVersionsView/projectVersionsView.component.tsx b/apps/app/src/components/projectVersionsView/projectVersionsView.component.tsx new file mode 100644 index 0000000..ac42315 --- /dev/null +++ b/apps/app/src/components/projectVersionsView/projectVersionsView.component.tsx @@ -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 ( + <> +
    {project.name}
    +
    + {project.environments.edges.map((environmentNode, key) => { + const env = environmentNode?.node; + return ( +
    +
    +
    Environment name
    +
    {env?.name}
    + + Open environment + +
    +
    +
    Version
    +
    1.0.{key - 1}
    +
    Updated: 6.05.23 16:41:11
    +
    + +
    + ); + })} +
    + +
    + {project.environments.edges.map((environmentNode, key) => { + const env = environmentNode?.node; + return ( +
    +
    +
    Environment services
    +
    + {project.services.edges.map((serviceNode, key) => { + const service = serviceNode?.node; + return ( +
    +
    {service?.name}
    +
    +
    Updated: 6.05.23 16:41:11
    +
    + ); + })} +
    + ); + })} +
    + + ); +}; diff --git a/apps/app/src/components/projectVersionsView/projectVersionsView.graphql.ts b/apps/app/src/components/projectVersionsView/projectVersionsView.graphql.ts new file mode 100644 index 0000000..1213efe --- /dev/null +++ b/apps/app/src/components/projectVersionsView/projectVersionsView.graphql.ts @@ -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 + } + } + } + } + } +`); diff --git a/apps/app/src/components/projects/projects.component.tsx b/apps/app/src/components/projects/projects.component.tsx deleted file mode 100644 index 5c4183f..0000000 --- a/apps/app/src/components/projects/projects.component.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useQuery } from '@apollo/client'; -import React from 'react'; - -import { projectsQuery } from './projects.graphql'; - -export const ProjectsComponent = () => { - const { loading, data } = useQuery(projectsQuery); - - if (loading || !data) { - return null; - } - - return ( - <> - {data.projects.map((project) => ( -
    {project.name}
    - ))} - - ); -}; diff --git a/apps/app/src/components/projects/projects.graphql.ts b/apps/app/src/components/projects/projects.graphql.ts deleted file mode 100644 index 3620235..0000000 --- a/apps/app/src/components/projects/projects.graphql.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@vm/api-client/__generated/gql'; - -export const projectsQuery = gql(/* GraphQL */` - query projectsQuery { - projects { - id - name - } - } -`); \ No newline at end of file diff --git a/apps/app/src/graphql/index.ts b/apps/app/src/graphql/index.ts new file mode 100644 index 0000000..766b037 --- /dev/null +++ b/apps/app/src/graphql/index.ts @@ -0,0 +1 @@ +export * from './projects.graphql'; diff --git a/apps/app/src/graphql/projects.graphql.ts b/apps/app/src/graphql/projects.graphql.ts new file mode 100644 index 0000000..4f76060 --- /dev/null +++ b/apps/app/src/graphql/projects.graphql.ts @@ -0,0 +1,44 @@ +import { gql } from '@vm/api-client/__generated/gql'; + +export const projectFragment = gql(/* GraphQL */ ` + fragment projectFragment on Project { + id + name + environments { + edges { + node { + id + } + } + } + } +`); + +export const newProjectMutation = gql(/* GraphQL */ ` + mutation newProjectMutation($name: String!) { + createProject(input: { name: $name }) { + __typename + ... on ValidationError { + message + fieldErrors { + message + path + } + } + ... on MutationCreateProjectSuccess { + data { + ...projectFragment + } + } + } + } +`); + +export const projectsQuery = gql(/* GraphQL */ ` + query projectsQuery { + projects { + id + ...projectFragment + } + } +`); diff --git a/apps/app/src/pages/_app.tsx b/apps/app/src/pages/_app.tsx index d3bfe41..1a0eaf5 100644 --- a/apps/app/src/pages/_app.tsx +++ b/apps/app/src/pages/_app.tsx @@ -1,12 +1,21 @@ -import '@app/styles/globals.css' -import type { AppProps } from 'next/app' +import type { NextPage } from 'next'; +import type { AppProps } from 'next/app'; +import type { ReactElement, ReactNode } from 'react'; import { AppProviders } from '@app/providers/AppProviders'; +import '@app/styles/globals.css'; -export default function App({ Component, pageProps }: AppProps) { - return ( - - - - ); +export type NextPageWithLayout

    , IP = P> = NextPage & { + getLayout?: (page: ReactElement) => ReactNode; +}; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +export default function App({ Component, pageProps }: AppPropsWithLayout) { + // Use the layout defined at the page level, if available + const getLayout = Component.getLayout || ((page) => page); + + return {getLayout()}; } diff --git a/apps/app/src/pages/dashboard/index.tsx b/apps/app/src/pages/dashboard/index.tsx new file mode 100644 index 0000000..74f25e2 --- /dev/null +++ b/apps/app/src/pages/dashboard/index.tsx @@ -0,0 +1,10 @@ +import { getDashboardLayout } from '@app/components/layouts'; +import { NextPageWithLayout } from '@app/pages/_app'; + +const Dashboard: NextPageWithLayout = () => { + return

    Dashboard here
    ; +}; + +Dashboard.getLayout = getDashboardLayout; + +export default Dashboard; diff --git a/apps/app/src/pages/index.tsx b/apps/app/src/pages/index.tsx index a64653d..dd4bd9a 100644 --- a/apps/app/src/pages/index.tsx +++ b/apps/app/src/pages/index.tsx @@ -1,8 +1,6 @@ import { Inter } from 'next/font/google'; import Image from 'next/image'; -import { ProjectsComponent } from '../components/projects/projects.component'; - const inter = Inter({ subsets: ['latin'] }); export default function Home() { @@ -105,7 +103,6 @@ export default function Home() {

    - ); } diff --git a/apps/app/src/pages/projects/index.tsx b/apps/app/src/pages/projects/index.tsx new file mode 100644 index 0000000..f2c30c5 --- /dev/null +++ b/apps/app/src/pages/projects/index.tsx @@ -0,0 +1,15 @@ +import { ProjectsList } from '@app/components/dashboard/projectsList/projectsList.component'; +import { getDashboardLayout } from '@app/components/layouts'; +import { NextPageWithLayout } from '@app/pages/_app'; + +const Projects: NextPageWithLayout = () => { + return ( +
    + +
    + ); +}; + +Projects.getLayout = getDashboardLayout; + +export default Projects; diff --git a/apps/app/src/pages/projects/new.tsx b/apps/app/src/pages/projects/new.tsx new file mode 100644 index 0000000..d53f1c0 --- /dev/null +++ b/apps/app/src/pages/projects/new.tsx @@ -0,0 +1,15 @@ +import { NewProjectForm } from '@app/components/dashboard/forms'; +import { getDashboardLayout } from '@app/components/layouts'; +import { NextPageWithLayout } from '@app/pages/_app'; + +const Projects: NextPageWithLayout = () => { + return ( +
    + +
    + ); +}; + +Projects.getLayout = getDashboardLayout; + +export default Projects; diff --git a/apps/app/src/pages/version/[id].tsx b/apps/app/src/pages/version/[id].tsx new file mode 100644 index 0000000..7126846 --- /dev/null +++ b/apps/app/src/pages/version/[id].tsx @@ -0,0 +1,56 @@ +import { useRouter } from 'next/router'; + +import { ProjectVersionsView } from '../../components/projectVersionsView'; + +export default function Version() { + const router = useRouter(); + return ( +
    + + + {/*
    Sample project
    */} + {/*
    */} + {/* {[1, 2, 3].map((key) => (*/} + {/*
    */} + {/*
    */} + {/*
    Environment name
    */} + {/*
    */} + {/*
    Env {key}
    */} + {/*
    */} + {/* */} + {/* Open environment*/} + {/* */} + {/*
    */} + {/*
    */} + {/*
    Version
    */} + {/*
    1.0.{key - 1}
    */} + {/*
    Updated: 6.05.23 16:41:11
    */} + {/*
    */} + {/* */} + {/*
    */} + {/* ))}*/} + {/*
    */} + + {/*
    */} + {/* {[1, 2, 3].map((key) => (*/} + {/*
    */} + {/*
    */} + {/*
    Environment services
    */} + {/*
    */} + {/* {[1, 2, 3].map((key) => (*/} + {/*
    */} + {/*
    Service {key}
    */} + {/*
    1.0.{key - 1}
    */} + {/*
    Updated: 6.05.23 16:41:11
    */} + {/*
    */} + {/* ))}*/} + {/*
    */} + {/* ))}*/} + {/*
    */} +
    + ); +} diff --git a/packages/api-client/__generated/gql/gql.ts b/packages/api-client/__generated/gql/gql.ts index 48531c7..3a31914 100644 --- a/packages/api-client/__generated/gql/gql.ts +++ b/packages/api-client/__generated/gql/gql.ts @@ -13,7 +13,10 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - "\n query projectsQuery {\n projects {\n id\n name\n }\n }\n": types.ProjectsQueryDocument, + "\n query projectQuery($id: ID!) {\n project(id: $id) {\n id\n name\n environments {\n edges {\n node {\n id\n name\n currentVersion {\n name\n id\n createdAt\n }\n }\n }\n }\n services {\n edges {\n node {\n name\n }\n }\n }\n }\n }\n": types.ProjectQueryDocument, + "\n fragment projectFragment on Project {\n id\n name\n environments {\n edges {\n node {\n id\n }\n }\n }\n }\n": types.ProjectFragmentFragmentDoc, + "\n mutation newProjectMutation($name: String!) {\n createProject(input: { name: $name }) {\n __typename\n ... on ValidationError {\n message\n fieldErrors {\n message\n path\n }\n }\n ... on MutationCreateProjectSuccess {\n data {\n ...projectFragment\n }\n }\n }\n }\n": types.NewProjectMutationDocument, + "\n query projectsQuery {\n projects {\n id\n ...projectFragment\n }\n }\n": types.ProjectsQueryDocument, }; /** @@ -33,7 +36,19 @@ export function gql(source: string): unknown; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\n query projectsQuery {\n projects {\n id\n name\n }\n }\n"): (typeof documents)["\n query projectsQuery {\n projects {\n id\n name\n }\n }\n"]; +export function gql(source: "\n query projectQuery($id: ID!) {\n project(id: $id) {\n id\n name\n environments {\n edges {\n node {\n id\n name\n currentVersion {\n name\n id\n createdAt\n }\n }\n }\n }\n services {\n edges {\n node {\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query projectQuery($id: ID!) {\n project(id: $id) {\n id\n name\n environments {\n edges {\n node {\n id\n name\n currentVersion {\n name\n id\n createdAt\n }\n }\n }\n }\n services {\n edges {\n node {\n name\n }\n }\n }\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n fragment projectFragment on Project {\n id\n name\n environments {\n edges {\n node {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n fragment projectFragment on Project {\n id\n name\n environments {\n edges {\n node {\n id\n }\n }\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation newProjectMutation($name: String!) {\n createProject(input: { name: $name }) {\n __typename\n ... on ValidationError {\n message\n fieldErrors {\n message\n path\n }\n }\n ... on MutationCreateProjectSuccess {\n data {\n ...projectFragment\n }\n }\n }\n }\n"): (typeof documents)["\n mutation newProjectMutation($name: String!) {\n createProject(input: { name: $name }) {\n __typename\n ... on ValidationError {\n message\n fieldErrors {\n message\n path\n }\n }\n ... on MutationCreateProjectSuccess {\n data {\n ...projectFragment\n }\n }\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n query projectsQuery {\n projects {\n id\n ...projectFragment\n }\n }\n"): (typeof documents)["\n query projectsQuery {\n projects {\n id\n ...projectFragment\n }\n }\n"]; export function gql(source: string) { return (documents as any)[source] ?? {}; diff --git a/packages/api-client/__generated/gql/graphql.ts b/packages/api-client/__generated/gql/graphql.ts index 65dd9b8..bcd861c 100644 --- a/packages/api-client/__generated/gql/graphql.ts +++ b/packages/api-client/__generated/gql/graphql.ts @@ -12,6 +12,10 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; + /** A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + Date: any; + /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ + DateTime: any; }; export type Environment = Node & { @@ -31,6 +35,11 @@ export type EnvironmentVersionsArgs = { last?: InputMaybe; }; +export type EnvironmentInput = { + name: Scalars['String']; + projectId: Scalars['String']; +}; + export type EnvironmentVersionsConnection = { __typename?: 'EnvironmentVersionsConnection'; edges: Array>; @@ -43,6 +52,66 @@ export type EnvironmentVersionsConnectionEdge = { node: Version; }; +export type Error = { + message: Scalars['String']; +}; + +export type Mutation = { + __typename?: 'Mutation'; + createEnvironment: MutationCreateEnvironmentResult; + createProject: MutationCreateProjectResult; + createService: MutationCreateServiceResult; + createVersion: MutationCreateVersionResult; +}; + + +export type MutationCreateEnvironmentArgs = { + input: EnvironmentInput; +}; + + +export type MutationCreateProjectArgs = { + input: ProjectInput; +}; + + +export type MutationCreateServiceArgs = { + input: ServiceInput; +}; + + +export type MutationCreateVersionArgs = { + input: VersionInput; +}; + +export type MutationCreateEnvironmentResult = MutationCreateEnvironmentSuccess | ValidationError; + +export type MutationCreateEnvironmentSuccess = { + __typename?: 'MutationCreateEnvironmentSuccess'; + data: Environment; +}; + +export type MutationCreateProjectResult = MutationCreateProjectSuccess | ValidationError; + +export type MutationCreateProjectSuccess = { + __typename?: 'MutationCreateProjectSuccess'; + data: Project; +}; + +export type MutationCreateServiceResult = MutationCreateServiceSuccess | ValidationError; + +export type MutationCreateServiceSuccess = { + __typename?: 'MutationCreateServiceSuccess'; + data: Service; +}; + +export type MutationCreateVersionResult = MutationCreateVersionSuccess | ValidationError; + +export type MutationCreateVersionSuccess = { + __typename?: 'MutationCreateVersionSuccess'; + data: Version; +}; + export type Node = { id: Scalars['ID']; }; @@ -91,6 +160,10 @@ export type ProjectEnvironmentsConnectionEdge = { node: Environment; }; +export type ProjectInput = { + name: Scalars['String']; +}; + export type ProjectServicesConnection = { __typename?: 'ProjectServicesConnection'; edges: Array>; @@ -107,7 +180,7 @@ export type Query = { __typename?: 'Query'; node?: Maybe; nodes: Array>; - project: Array; + project: Project; projects: Array; }; @@ -143,9 +216,14 @@ export type ServiceVersionsArgs = { last?: InputMaybe; }; +export type ServiceInput = { + name: Scalars['String']; + projectId: Scalars['String']; +}; + export type ServiceVersion = Node & { __typename?: 'ServiceVersion'; - createdAt: Scalars['String']; + createdAt: Scalars['DateTime']; id: Scalars['ID']; isCurrent: Scalars['Boolean']; name: Scalars['String']; @@ -165,9 +243,21 @@ export type ServiceVersionsConnectionEdge = { node: ServiceVersion; }; +export type ValidationError = Error & { + __typename?: 'ValidationError'; + fieldErrors: Array; + message: Scalars['String']; +}; + +export type ValidationFieldError = { + __typename?: 'ValidationFieldError'; + message: Scalars['String']; + path: Array; +}; + export type Version = Node & { __typename?: 'Version'; - createdAt: Scalars['String']; + createdAt: Scalars['DateTime']; environment: Environment; id: Scalars['ID']; isCurrent: Scalars['Boolean']; @@ -183,6 +273,12 @@ export type VersionServiceVersionsArgs = { last?: InputMaybe; }; +export type VersionInput = { + environmentId: Scalars['String']; + isCurrent: Scalars['Boolean']; + name: Scalars['String']; +}; + export type VersionServiceVersionsConnection = { __typename?: 'VersionServiceVersionsConnection'; edges: Array>; @@ -195,10 +291,34 @@ export type VersionServiceVersionsConnectionEdge = { node: ServiceVersion; }; -export type ProjectsQueryQueryVariables = Exact<{ [key: string]: never; }>; +export type ProjectQueryQueryVariables = Exact<{ + id: Scalars['ID']; +}>; + +export type ProjectQueryQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, environments: { __typename?: 'ProjectEnvironmentsConnection', edges: Array<{ __typename?: 'ProjectEnvironmentsConnectionEdge', node: { __typename?: 'Environment', id: string, name: string, currentVersion?: { __typename?: 'Version', name: string, id: string, createdAt: any } | null } } | null> }, services: { __typename?: 'ProjectServicesConnection', edges: Array<{ __typename?: 'ProjectServicesConnectionEdge', node: { __typename?: 'Service', name: string } } | null> } } }; + +export type ProjectFragmentFragment = { __typename?: 'Project', id: string, name: string, environments: { __typename?: 'ProjectEnvironmentsConnection', edges: Array<{ __typename?: 'ProjectEnvironmentsConnectionEdge', node: { __typename?: 'Environment', id: string } } | null> } } & { ' $fragmentName'?: 'ProjectFragmentFragment' }; + +export type NewProjectMutationMutationVariables = Exact<{ + name: Scalars['String']; +}>; + + +export type NewProjectMutationMutation = { __typename?: 'Mutation', createProject: { __typename: 'MutationCreateProjectSuccess', data: ( + { __typename?: 'Project' } + & { ' $fragmentRefs'?: { 'ProjectFragmentFragment': ProjectFragmentFragment } } + ) } | { __typename: 'ValidationError', message: string, fieldErrors: Array<{ __typename?: 'ValidationFieldError', message: string, path: Array }> } }; + +export type ProjectsQueryQueryVariables = Exact<{ [key: string]: never; }>; -export type ProjectsQueryQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'Project', id: string, name: string }> }; +export type ProjectsQueryQuery = { __typename?: 'Query', projects: Array<( + { __typename?: 'Project', id: string } + & { ' $fragmentRefs'?: { 'ProjectFragmentFragment': ProjectFragmentFragment } } + )> }; -export const ProjectsQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"projectsQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ProjectFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"projectFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const ProjectQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"projectQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"currentVersion"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const NewProjectMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"newProjectMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ValidationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fieldErrors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateProjectSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"projectFragment"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"projectFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const ProjectsQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"projectsQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"projectFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"projectFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/api-client/graphql/schema/api.graphql b/packages/api-client/graphql/schema/api.graphql index c3305a8..bb3adf0 100644 --- a/packages/api-client/graphql/schema/api.graphql +++ b/packages/api-client/graphql/schema/api.graphql @@ -1,3 +1,11 @@ +schema { + query: Query + mutation: Mutation +} +"A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar." +scalar Date +"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar." +scalar DateTime type Environment implements Node { currentVersion: Version id: ID! @@ -13,6 +21,24 @@ type EnvironmentVersionsConnectionEdge { cursor: String! node: Version! } +type Mutation { + createEnvironment(input: EnvironmentInput!): MutationCreateEnvironmentResult! + createProject(input: ProjectInput!): MutationCreateProjectResult! + createService(input: ServiceInput!): MutationCreateServiceResult! + createVersion(input: VersionInput!): MutationCreateVersionResult! +} +type MutationCreateEnvironmentSuccess { + data: Environment! +} +type MutationCreateProjectSuccess { + data: Project! +} +type MutationCreateServiceSuccess { + data: Service! +} +type MutationCreateVersionSuccess { + data: Version! +} type PageInfo { endCursor: String hasNextPage: Boolean! @@ -44,7 +70,7 @@ type ProjectServicesConnectionEdge { type Query { node(id: ID!): Node nodes(ids: [ID!]!): [Node]! - project(id: ID!): [Project!]! + project(id: ID!): Project! projects: [Project!]! } type Service implements Node { @@ -55,7 +81,7 @@ type Service implements Node { versions(after: String, before: String, first: Int, last: Int): ServiceVersionsConnection! } type ServiceVersion implements Node { - createdAt: String! + createdAt: DateTime! id: ID! isCurrent: Boolean! name: String! @@ -70,8 +96,16 @@ type ServiceVersionsConnectionEdge { cursor: String! node: ServiceVersion! } +type ValidationError implements Error { + fieldErrors: [ValidationFieldError!]! + message: String! +} +type ValidationFieldError { + message: String! + path: [String!]! +} type Version implements Node { - createdAt: String! + createdAt: DateTime! environment: Environment! id: ID! isCurrent: Boolean! @@ -86,9 +120,32 @@ type VersionServiceVersionsConnectionEdge { cursor: String! node: ServiceVersion! } +interface Error { + message: String! +} interface Node { id: ID! } +union MutationCreateEnvironmentResult = MutationCreateEnvironmentSuccess | ValidationError +union MutationCreateProjectResult = MutationCreateProjectSuccess | ValidationError +union MutationCreateServiceResult = MutationCreateServiceSuccess | ValidationError +union MutationCreateVersionResult = MutationCreateVersionSuccess | ValidationError +input EnvironmentInput { + name: String! + projectId: String! +} +input ProjectInput { + name: String! +} +input ServiceInput { + name: String! + projectId: String! +} +input VersionInput { + environmentId: String! + isCurrent: Boolean! + name: String! +} "Exposes a URL that specifies the behavior of this scalar." directive @specifiedBy( "The URL that specifies the behavior of this scalar." diff --git a/packages/graphql/schema/project.ts b/packages/graphql/schema/project.ts index fcf17db..a752951 100644 --- a/packages/graphql/schema/project.ts +++ b/packages/graphql/schema/project.ts @@ -57,12 +57,12 @@ builder.queryField('projects', (t) => builder.queryField('project', (t) => t.prismaField({ - type: ['Project'], + type: 'Project', args: { id: t.arg.globalID({ required: true }), }, resolve: async (query, _parent, _args, _info) => - prismaClient.project.findMany({ + prismaClient.project.findFirstOrThrow({ ...query, where: { id: String(_args.id.id), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bb8868..5d7e5ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,7 @@ importers: 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 dependencies: @@ -83,6 +84,7 @@ importers: postcss: 8.4.23 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + react-hook-form: 7.43.9_react@18.2.0 tailwindcss: 3.3.2 typescript: 5.0.4 devDependencies: @@ -5570,6 +5572,15 @@ packages: scheduler: 0.23.0 dev: false + /react-hook-form/7.43.9_react@18.2.0: + resolution: {integrity: sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}