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 path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
@ -131,8 +131,7 @@ export class Database {
|
|||||||
return environmentVariables;
|
return environmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberByMemberId (memberId: string): Promise<boolean> {
|
async removeProjectMemberById (memberId: string): Promise<boolean> {
|
||||||
// TODO: Check if user is authorized to delete members
|
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const deleted = await projectMemberRepository.delete(memberId);
|
const deleted = await projectMemberRepository.delete(memberId);
|
||||||
@ -206,4 +205,15 @@ export class Database {
|
|||||||
|
|
||||||
return projects;
|
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 { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
|
|
||||||
enum Environment {
|
export enum Environment {
|
||||||
Production = 'Production',
|
Production = 'Production',
|
||||||
Preview = 'Preview',
|
Preview = 'Preview',
|
||||||
Development = 'Development',
|
Development = 'Development',
|
||||||
|
@ -3,6 +3,7 @@ import assert from 'assert';
|
|||||||
|
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
|
import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils';
|
||||||
|
import { Environment } from './entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@ -102,7 +103,7 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
|||||||
assert(memberProject);
|
assert(memberProject);
|
||||||
|
|
||||||
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
|
if (isUserOwner(String(context.userId), String(memberProject.owner.id))) {
|
||||||
return db.removeProjectMemberByMemberId(memberId);
|
return db.removeProjectMemberById(memberId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid operation: not authorized');
|
throw new Error('Invalid operation: not authorized');
|
||||||
}
|
}
|
||||||
@ -119,6 +120,17 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
|||||||
log(err);
|
log(err);
|
||||||
return false;
|
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 {
|
type Mutation {
|
||||||
removeMember(memberId: String!): Boolean!
|
removeMember(memberId: String!): Boolean!
|
||||||
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
||||||
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input AddEnvironmentVariableInput {
|
input AddEnvironmentVariableInput {
|
||||||
|
@ -117,6 +117,8 @@ const main = async () => {
|
|||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
|
|
||||||
await generateTestData(dataSource);
|
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';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
@ -8,18 +8,48 @@ import FilterForm, {
|
|||||||
StatusOptions,
|
StatusOptions,
|
||||||
} from './deployments/FilterForm';
|
} from './deployments/FilterForm';
|
||||||
import { DeploymentDetails } from '../../../types/project';
|
import { DeploymentDetails } from '../../../types/project';
|
||||||
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
|
||||||
const DEFAULT_FILTER_VALUE: FilterValue = {
|
const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||||
searchedBranch: '',
|
searchedBranch: '',
|
||||||
status: StatusOptions.ALL_STATUS,
|
status: StatusOptions.ALL_STATUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeploymentsTabPanel = ({
|
const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => {
|
||||||
deployments,
|
const client = useGQLClient();
|
||||||
}: {
|
|
||||||
deployments: DeploymentDetails[];
|
|
||||||
}) => {
|
|
||||||
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
|
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(() => {
|
const productionDeployment = useMemo(() => {
|
||||||
return deployments.find((deployment) => {
|
return deployments.find((deployment) => {
|
||||||
@ -49,12 +79,16 @@ const DeploymentsTabPanel = ({
|
|||||||
|
|
||||||
return branchMatch && statusMatch && dateMatch;
|
return branchMatch && statusMatch && dateMatch;
|
||||||
}) as DeploymentDetails[];
|
}) as DeploymentDetails[];
|
||||||
}, [filterValue]);
|
}, [filterValue, deployments]);
|
||||||
|
|
||||||
const handleResetFilters = useCallback(() => {
|
const handleResetFilters = useCallback(() => {
|
||||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onUpdateDeploymenToProd = async () => {
|
||||||
|
await fetchDeployments();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<FilterForm
|
<FilterForm
|
||||||
@ -69,6 +103,7 @@ const DeploymentsTabPanel = ({
|
|||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
key={key}
|
key={key}
|
||||||
productionDeployment={productionDeployment}
|
productionDeployment={productionDeployment}
|
||||||
|
onUpdate={onUpdateDeploymenToProd}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -46,7 +46,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
|
|||||||
<OverviewTabPanel project={project} />
|
<OverviewTabPanel project={project} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<DeploymentsTabPanel deployments={project.deployments} />
|
<DeploymentsTabPanel projectId={project.id} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<Database />
|
<Database />
|
||||||
|
@ -9,15 +9,18 @@ import {
|
|||||||
Chip,
|
Chip,
|
||||||
ChipProps,
|
ChipProps,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||||
import { DeploymentDetails, Status } from '../../../../types/project';
|
import { DeploymentDetails, Status } from '../../../../types/project';
|
||||||
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: DeploymentDetails;
|
deployment: DeploymentDetails;
|
||||||
productionDeployment: DeploymentDetails;
|
productionDeployment: DeploymentDetails;
|
||||||
|
onUpdate: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
||||||
@ -29,11 +32,24 @@ const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
|
|||||||
const DeploymentDetailsCard = ({
|
const DeploymentDetailsCard = ({
|
||||||
deployment,
|
deployment,
|
||||||
productionDeployment,
|
productionDeployment,
|
||||||
|
onUpdate,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||||
const [rollbackDeployment, setRollbackDeployment] = 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 (
|
return (
|
||||||
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
|
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
@ -97,7 +113,10 @@ const DeploymentDetailsCard = ({
|
|||||||
open={changeToProduction}
|
open={changeToProduction}
|
||||||
confirmButtonTitle="Change"
|
confirmButtonTitle="Change"
|
||||||
color="blue"
|
color="blue"
|
||||||
handleConfirm={() => setChangeToProduction((preVal) => !preVal)}
|
handleConfirm={() => {
|
||||||
|
updateDeployment();
|
||||||
|
setChangeToProduction((preVal) => !preVal);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
|
@ -33,6 +33,7 @@ export interface ProjectMember {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DeploymentDetails {
|
export interface DeploymentDetails {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
isProduction: boolean;
|
isProduction: boolean;
|
||||||
domain: DomainDetails;
|
domain: DomainDetails;
|
||||||
|
@ -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, getEnvironmentVariables } from './queries';
|
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables } from './queries';
|
||||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types';
|
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse } from './types';
|
||||||
import { removeMember, addEnvironmentVariables } from './mutations';
|
import { removeMember, addEnvironmentVariables, updateDeploymentToProd } from './mutations';
|
||||||
|
|
||||||
export interface GraphQLConfig {
|
export interface GraphQLConfig {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -113,4 +113,15 @@ export class GQLClient {
|
|||||||
|
|
||||||
return data;
|
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)
|
addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const updateDeploymentToProd = gql`
|
||||||
|
mutation ($deploymentId: String!) {
|
||||||
|
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -165,3 +165,7 @@ export type AddEnvironmentVariableInput = {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateDeploymentToProdResponse = {
|
||||||
|
updateDeploymentToProd: boolean;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user