forked from cerc-io/snowballtools-base
Add GQL mutation to update deployment to production (#39)
* Add mutation to update deployment to production * Implement gql client mutation and frontend to update deployment to production * Add toast message when deployment is changed to production * Throw error from init db script if db aleardy exists --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
44310d4eb8
commit
2fb048e8ab
@ -1,4 +1,4 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DataSource, DeepPartial } from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
@ -131,8 +131,7 @@ export class Database {
|
||||
return environmentVariables;
|
||||
}
|
||||
|
||||
async removeProjectMemberByMemberId (memberId: string): Promise<boolean> {
|
||||
// TODO: Check if user is authorized to delete members
|
||||
async removeProjectMemberById (memberId: string): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const deleted = await projectMemberRepository.delete(memberId);
|
||||
@ -206,4 +205,15 @@ export class Database {
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async updateDeploymentById (deploymentId: string, updates: DeepPartial<Deployment>): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const updatedDeployment = await deploymentRepository.update({ id: Number(deploymentId) }, updates);
|
||||
|
||||
if (updatedDeployment.affected) {
|
||||
return updatedDeployment.affected > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
import { Project } from './Project';
|
||||
import { Domain } from './Domain';
|
||||
|
||||
enum Environment {
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development',
|
||||
|
@ -3,6 +3,7 @@ import assert from 'assert';
|
||||
|
||||
import { Database } from './database';
|
||||
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
|
||||
import { Environment } from './entity/Deployment';
|
||||
|
||||
const log = debug('snowball:database');
|
||||
|
||||
@ -102,7 +103,7 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
||||
assert(memberProject);
|
||||
|
||||
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
|
||||
return db.removeProjectMemberByMemberId(memberId);
|
||||
return db.removeProjectMemberById(memberId);
|
||||
} else {
|
||||
throw new Error('Invalid operation: not authorized');
|
||||
}
|
||||
@ -119,6 +120,17 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateDeploymentToProd: async (_: any, { deploymentId }: {deploymentId: string }) => {
|
||||
try {
|
||||
return db.updateDeploymentById(deploymentId, {
|
||||
environment: Environment.Production
|
||||
});
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -124,6 +124,7 @@ type Query {
|
||||
type Mutation {
|
||||
removeMember(memberId: String!): Boolean!
|
||||
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
}
|
||||
|
||||
input AddEnvironmentVariableInput {
|
||||
|
@ -117,6 +117,8 @@ const main = async () => {
|
||||
await dataSource.initialize();
|
||||
|
||||
await generateTestData(dataSource);
|
||||
} else {
|
||||
throw new Error('Database already exists');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
@ -8,18 +8,48 @@ import FilterForm, {
|
||||
StatusOptions,
|
||||
} from './deployments/FilterForm';
|
||||
import { DeploymentDetails } from '../../../types/project';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
|
||||
const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||
searchedBranch: '',
|
||||
status: StatusOptions.ALL_STATUS,
|
||||
};
|
||||
|
||||
const DeploymentsTabPanel = ({
|
||||
deployments,
|
||||
}: {
|
||||
deployments: DeploymentDetails[];
|
||||
}) => {
|
||||
const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
|
||||
const [deployments, setDeployments] = useState<DeploymentDetails[]>([]);
|
||||
|
||||
const fetchDeployments = async () => {
|
||||
const { deployments } = await client.getDeployments(projectId);
|
||||
const updatedDeployments = deployments.map((deployment: any) => {
|
||||
return {
|
||||
...deployment,
|
||||
isProduction: deployment.environment === 'Production',
|
||||
author: '',
|
||||
commit: {
|
||||
hash: '',
|
||||
message: '',
|
||||
},
|
||||
domain: deployment.domain
|
||||
? {
|
||||
...deployment.domain,
|
||||
record: {
|
||||
type: '',
|
||||
name: '',
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
setDeployments(updatedDeployments);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDeployments();
|
||||
}, []);
|
||||
|
||||
const productionDeployment = useMemo(() => {
|
||||
return deployments.find((deployment) => {
|
||||
@ -49,12 +79,16 @@ const DeploymentsTabPanel = ({
|
||||
|
||||
return branchMatch && statusMatch && dateMatch;
|
||||
}) as DeploymentDetails[];
|
||||
}, [filterValue]);
|
||||
}, [filterValue, deployments]);
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||
}, []);
|
||||
|
||||
const onUpdateDeploymenToProd = async () => {
|
||||
await fetchDeployments();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<FilterForm
|
||||
@ -69,6 +103,7 @@ const DeploymentsTabPanel = ({
|
||||
deployment={deployment}
|
||||
key={key}
|
||||
productionDeployment={productionDeployment}
|
||||
onUpdate={onUpdateDeploymenToProd}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -46,7 +46,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
|
||||
<OverviewTabPanel project={project} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<DeploymentsTabPanel deployments={project.deployments} />
|
||||
<DeploymentsTabPanel projectId={project.id} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Database />
|
||||
|
@ -9,15 +9,18 @@ import {
|
||||
Chip,
|
||||
ChipProps,
|
||||
} from '@material-tailwind/react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import { DeploymentDetails, Status } from '../../../../types/project';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: DeploymentDetails;
|
||||
productionDeployment: DeploymentDetails;
|
||||
onUpdate: () => Promise<void>;
|
||||
}
|
||||
|
||||
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
||||
@ -29,11 +32,24 @@ const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
||||
const DeploymentDetailsCard = ({
|
||||
deployment,
|
||||
productionDeployment,
|
||||
onUpdate,
|
||||
}: DeployDetailsCardProps) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast.success('Deployment changed to production');
|
||||
} else {
|
||||
toast.error('Unable to change deployment to production');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
|
||||
<div className="col-span-2">
|
||||
@ -97,7 +113,10 @@ const DeploymentDetailsCard = ({
|
||||
open={changeToProduction}
|
||||
confirmButtonTitle="Change"
|
||||
color="blue"
|
||||
handleConfirm={() => setChangeToProduction((preVal) => !preVal)}
|
||||
handleConfirm={() => {
|
||||
updateDeployment();
|
||||
setChangeToProduction((preVal) => !preVal);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
|
@ -33,6 +33,7 @@ export interface ProjectMember {
|
||||
}
|
||||
|
||||
export interface DeploymentDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
isProduction: boolean;
|
||||
domain: DomainDetails;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
|
||||
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';
|
||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse } from './types';
|
||||
import { removeMember, addEnvironmentVariables, updateDeploymentToProd } from './mutations';
|
||||
|
||||
export interface GraphQLConfig {
|
||||
gqlEndpoint: string;
|
||||
@ -113,4 +113,15 @@ export class GQLClient {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateDeploymentToProd (deploymentId: string): Promise<UpdateDeploymentToProdResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: updateDeploymentToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
@ -11,3 +11,9 @@ mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInp
|
||||
addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables)
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateDeploymentToProd = gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
|
@ -165,3 +165,7 @@ export type AddEnvironmentVariableInput = {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type UpdateDeploymentToProdResponse = {
|
||||
updateDeploymentToProd: boolean;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user