Add and refresh environment variables in project settings tab (#37)

* Create and use add environment variables gql client method

* Implement get environment variables method

* Display fetched environment variables

* Refactor create environment variables submit handler

* Use environment variables type from gql-client

* Add fixtures for project member

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-25 10:44:51 +05:30 committed by Ashwin Phatak
parent c2b997a17b
commit 44310d4eb8
12 changed files with 191 additions and 61 deletions

View File

@ -58,6 +58,16 @@ export const createResolvers = async (db: Database): Promise<any> => {
return deployments; return deployments;
}, },
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
const dbEnvironmentVariables = await db.getEnvironmentVariablesByProjectId(projectId);
const environmentVariables = dbEnvironmentVariables.map(dbEnvironmentVariable => {
return environmentVariableToGqlType(dbEnvironmentVariable);
});
return environmentVariables;
},
projectMembers: async (_: any, { projectId }: { projectId: string }) => { projectMembers: async (_: any, { projectId }: { projectId: string }) => {
const dbProjectMembers = await db.getProjectMembersByProjectId(projectId); const dbProjectMembers = await db.getProjectMembersByProjectId(projectId);

View File

@ -116,6 +116,7 @@ type Query {
organizations: [Organization!] organizations: [Organization!]
projects: [Project!] projects: [Project!]
deployments(projectId: String!): [Deployment!] deployments(projectId: String!): [Deployment!]
environmentVariables(projectId: String!): [EnvironmentVariable!]
projectMembers(projectId: String!): [ProjectMember!] projectMembers(projectId: String!): [ProjectMember!]
searchProjects(searchText: String!): [Project!] searchProjects(searchText: String!): [Project!]
} }

View File

@ -2,11 +2,46 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View", "Edit"] "permissions": ["View"]
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": ["View", "Edit"] "permissions": ["View", "Edit"]
},
{
"memberIndex": 2,
"projectIndex": 1,
"permissions": ["View"]
},
{
"memberIndex": 0,
"projectIndex": 2,
"permissions": ["View"]
},
{
"memberIndex": 1,
"projectIndex": 2,
"permissions": ["View", "Edit"]
},
{
"memberIndex": 0,
"projectIndex": 3,
"permissions": ["View"]
},
{
"memberIndex": 2,
"projectIndex": 3,
"permissions": ["View", "Edit"]
},
{
"memberIndex": 1,
"projectIndex": 4,
"permissions": ["View"]
},
{
"memberIndex": 2,
"projectIndex": 4,
"permissions": ["View", "Edit"]
} }
] ]

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Collapse, Typography } from '@material-tailwind/react'; import { Card, Collapse, Typography } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow'; import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environments, EnvironmentVariable } from '../../../../types/project';
interface DisplayEnvironmentVariablesProps { interface DisplayEnvironmentVariablesProps {
environment: Environments; environment: Environment;
variables: EnvironmentVariable[]; variables: EnvironmentVariable[];
} }

View File

@ -1,11 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { IconButton, Input, Typography } from '@material-tailwind/react'; import { IconButton, Input, Typography } from '@material-tailwind/react';
import { EnvironmentVariable } from 'gql-client';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
import { EnvironmentVariable } from '../../../../types/project';
const ShowPasswordIcon = ({ const ShowPasswordIcon = ({
handler, handler,

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useOutletContext, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
Typography, Typography,
@ -12,14 +12,12 @@ import {
Chip, Chip,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client';
import AddEnvironmentVariableRow from './AddEnvironmentVariableRow'; import AddEnvironmentVariableRow from './AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from './DisplayEnvironmentVariables'; import DisplayEnvironmentVariables from './DisplayEnvironmentVariables';
import {
EnvironmentVariable,
Environments,
ProjectSearchOutletContext,
} from '../../../../types/project';
import HorizontalLine from '../../../HorizontalLine'; import HorizontalLine from '../../../HorizontalLine';
import { useGQLClient } from '../../../../context/GQLClientContext';
export type EnvironmentVariablesFormValues = { export type EnvironmentVariablesFormValues = {
variables: { variables: {
@ -35,14 +33,11 @@ export type EnvironmentVariablesFormValues = {
export const EnvironmentVariablesTabPanel = () => { export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams(); const { id } = useParams();
const client = useGQLClient();
const { projects } = useOutletContext<ProjectSearchOutletContext>(); const [environmentVariables, setEnvironmentVariables] = useState<
EnvironmentVariable[]
const currentProject = useMemo(() => { >([]);
return projects.find((project) => {
return project.id === id;
});
}, [id, projects]);
const { const {
handleSubmit, handleSubmit,
@ -74,13 +69,16 @@ export const EnvironmentVariablesTabPanel = () => {
if (isSubmitSuccessful) { if (isSubmitSuccessful) {
reset(); reset();
} }
}, [isSubmitSuccessful, reset]); }, [isSubmitSuccessful, reset, id]);
const getEnvironmentVariable = useCallback((environment: Environments) => { const getEnvironmentVariable = useCallback(
return ( (environment: Environment) => {
currentProject?.environmentVariables as EnvironmentVariable[] return environmentVariables.filter((item) =>
).filter((item) => item.environments.includes(environment)); item.environments.includes(environment),
}, []); );
},
[environmentVariables, id],
);
const isFieldEmpty = useMemo(() => { const isFieldEmpty = useMemo(() => {
if (errors.variables) { if (errors.variables) {
@ -95,7 +93,55 @@ export const EnvironmentVariablesTabPanel = () => {
} }
return false; return false;
}, [fields, errors.variables]); }, [fields, errors.variables, id]);
const fetchEnvironmentVariables = useCallback(
async (id: string | undefined) => {
if (id) {
const { environmentVariables } =
await client.getEnvironmentVariables(id);
setEnvironmentVariables(environmentVariables);
}
},
[],
);
useEffect(() => {
fetchEnvironmentVariables(id);
}, [id]);
const createEnvironmentVariablesHandler = useCallback(
async (createFormData: EnvironmentVariablesFormValues) => {
const environmentVariables = createFormData.variables.map((variable) => {
return {
key: variable.key,
value: variable.value,
environments: Object.entries(createFormData.environment)
.filter(([, value]) => value === true)
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
};
});
const { addEnvironmentVariables: isEnvironmentVariablesAdded } =
await client.addEnvironmentVariables(id!, environmentVariables);
if (isEnvironmentVariablesAdded) {
reset();
setCreateNewVariable((cur) => !cur);
fetchEnvironmentVariables(id);
toast.success(
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
);
} else {
toast.error('Environment variables not added');
}
},
[id, client],
);
return ( return (
<> <>
@ -112,16 +158,7 @@ export const EnvironmentVariablesTabPanel = () => {
</div> </div>
<Collapse open={createNewVariable}> <Collapse open={createNewVariable}>
<Card className="bg-white p-2"> <Card className="bg-white p-2">
<form <form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
onSubmit={handleSubmit((data) => {
toast.success(
data.variables.length > 1
? `${data.variables.length} variables added`
: `Variable added`,
);
reset();
})}
>
{fields.map((field, index) => { {fields.map((field, index) => {
return ( return (
<AddEnvironmentVariableRow <AddEnvironmentVariableRow
@ -189,18 +226,18 @@ export const EnvironmentVariablesTabPanel = () => {
</div> </div>
<div className="p-2"> <div className="p-2">
<DisplayEnvironmentVariables <DisplayEnvironmentVariables
environment={Environments.PRODUCTION} environment={Environment.Production}
variables={getEnvironmentVariable(Environments.PRODUCTION)} variables={getEnvironmentVariable(Environment.Production)}
/> />
<HorizontalLine /> <HorizontalLine />
<DisplayEnvironmentVariables <DisplayEnvironmentVariables
environment={Environments.PREVIEW} environment={Environment.Preview}
variables={getEnvironmentVariable(Environments.PREVIEW)} variables={getEnvironmentVariable(Environment.Preview)}
/> />
<HorizontalLine /> <HorizontalLine />
<DisplayEnvironmentVariables <DisplayEnvironmentVariables
environment={Environments.DEVELOPMENT} environment={Environment.Development}
variables={getEnvironmentVariable(Environments.DEVELOPMENT)} variables={getEnvironmentVariable(Environment.Development)}
/> />
</div> </div>
</> </>

View File

@ -1,11 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { Environment } from 'gql-client';
import HorizontalLine from '../components/HorizontalLine'; import HorizontalLine from '../components/HorizontalLine';
import { IconButton, Typography } from '@material-tailwind/react'; import { IconButton, Typography } from '@material-tailwind/react';
import ProjectSearchBar from '../components/projects/ProjectSearchBar'; import ProjectSearchBar from '../components/projects/ProjectSearchBar';
import { useGQLClient } from '../context/GQLClientContext'; import { useGQLClient } from '../context/GQLClientContext';
import { Environments, ProjectDetails } from '../types/project'; import { ProjectDetails } from '../types/project';
const ProjectSearch = () => { const ProjectSearch = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -25,7 +27,7 @@ const ProjectSearch = () => {
const updatedDeployments = deployments.map((deployment: any) => { const updatedDeployments = deployments.map((deployment: any) => {
return { return {
...deployment, ...deployment,
isProduction: deployment.environment === Environments.PRODUCTION, isProduction: deployment.environment === Environment.Production,
author: '', author: '',
commit: { commit: {
hash: '', hash: '',

View File

@ -1,3 +1,5 @@
import { Environment, EnvironmentVariable } from 'gql-client';
export interface ProjectDetails { export interface ProjectDetails {
icon: string; icon: string;
name: string; name: string;
@ -36,7 +38,7 @@ export interface DeploymentDetails {
domain: DomainDetails; domain: DomainDetails;
status: Status; status: Status;
branch: string; branch: string;
environment: Environments; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
commit: { commit: {
hash: string; hash: string;
@ -52,19 +54,6 @@ export enum Status {
ERROR = 'Error', ERROR = 'Error',
} }
export enum Environments {
PRODUCTION = 'Production',
PREVIEW = 'Preview',
DEVELOPMENT = 'Development',
}
export interface EnvironmentVariable {
key: string;
value: string;
id: number;
environments: Environments[];
}
export interface RepositoryDetails { export interface RepositoryDetails {
title: string; title: string;
updatedAt: string; updatedAt: string;

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects } from './queries'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables } from './queries';
import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types'; import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types';
import { removeMember } from './mutations'; import { removeMember, addEnvironmentVariables } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -58,6 +58,17 @@ export class GQLClient {
return data; return data;
} }
async getEnvironmentVariables (projectId: string) : Promise<GetEnvironmentVariablesResponse> {
const { data } = await this.client.query({
query: getEnvironmentVariables,
variables: {
projectId
}
});
return data;
}
async removeMember (memberId: string): Promise<RemoveMemberResponse> { async removeMember (memberId: string): Promise<RemoveMemberResponse> {
const { data } = await this.client.mutate({ const { data } = await this.client.mutate({
mutation: removeMember, mutation: removeMember,
@ -90,4 +101,16 @@ export class GQLClient {
return data; return data;
} }
async addEnvironmentVariables (projectId: string, environmentVariables: AddEnvironmentVariableInput[]): Promise<AddEnvironmentVariablesResponse> {
const { data } = await this.client.mutate({
mutation: addEnvironmentVariables,
variables: {
projectId,
environmentVariables
}
});
return data;
}
} }

View File

@ -5,3 +5,9 @@ mutation ($memberId: String!) {
removeMember(memberId: $memberId) removeMember(memberId: $memberId)
} }
`; `;
export const addEnvironmentVariables = gql`
mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInput!]) {
addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables)
}
`;

View File

@ -82,6 +82,19 @@ query ($projectId: String!) {
} }
`; `;
export const getEnvironmentVariables = gql`
query ($projectId: String!) {
environmentVariables(projectId: $projectId) {
createdAt
environments
id
key
updatedAt
value
}
}
`;
export const getProjectMembers = gql` export const getProjectMembers = gql`
query ($projectId: String!) { query ($projectId: String!) {
projectMembers(projectId: $projectId) { projectMembers(projectId: $projectId) {

View File

@ -140,6 +140,10 @@ export type GetDeploymentsResponse = {
deployments: Deployment[] deployments: Deployment[]
} }
export type GetEnvironmentVariablesResponse = {
environmentVariables: EnvironmentVariable[]
}
export type GetOrganizationsResponse = { export type GetOrganizationsResponse = {
organizations: Organization[] organizations: Organization[]
} }
@ -151,3 +155,13 @@ export type GetUserResponse = {
export type SearchProjectsResponse = { export type SearchProjectsResponse = {
searchProjects: Project[] searchProjects: Project[]
} }
export type AddEnvironmentVariablesResponse = {
addEnvironmentVariables: boolean;
}
export type AddEnvironmentVariableInput = {
environments: string[];
key: string;
value: string;
}