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;
},
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 }) => {
const dbProjectMembers = await db.getProjectMembersByProjectId(projectId);

View File

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

View File

@ -2,11 +2,46 @@
{
"memberIndex": 1,
"projectIndex": 0,
"permissions": ["View", "Edit"]
"permissions": ["View"]
},
{
"memberIndex": 2,
"projectIndex": 0,
"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 { Card, Collapse, Typography } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environments, EnvironmentVariable } from '../../../../types/project';
interface DisplayEnvironmentVariablesProps {
environment: Environments;
environment: Environment;
variables: EnvironmentVariable[];
}

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { Environment, EnvironmentVariable } from 'gql-client';
export interface ProjectDetails {
icon: string;
name: string;
@ -36,7 +38,7 @@ export interface DeploymentDetails {
domain: DomainDetails;
status: Status;
branch: string;
environment: Environments;
environment: Environment;
isCurrent: boolean;
commit: {
hash: string;
@ -52,19 +54,6 @@ export enum Status {
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 {
title: string;
updatedAt: string;

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects } from './queries';
import { GetDeploymentsResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types';
import { removeMember } from './mutations';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables } from './queries';
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types';
import { removeMember, addEnvironmentVariables } from './mutations';
export interface GraphQLConfig {
gqlEndpoint: string;
@ -58,6 +58,17 @@ export class GQLClient {
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> {
const { data } = await this.client.mutate({
mutation: removeMember,
@ -90,4 +101,16 @@ export class GQLClient {
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)
}
`;
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`
query ($projectId: String!) {
projectMembers(projectId: $projectId) {

View File

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