Implement polling for project deployment updates (#87)

* Populate organization and user if db is empty

* Use hardcoded user id from fixtures

* Implement polling for get deployments query

* Handle review changes

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-21 15:34:33 +05:30 committed by GitHub
parent fc240c93d8
commit 8ca55cd888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 110 additions and 55 deletions

View File

@ -5,6 +5,6 @@ export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE
export const DEFAULT_GQL_PATH = '/graphql'; export const DEFAULT_GQL_PATH = '/graphql';
// Note: temporary hardcoded user, later to be derived from auth token // Note: temporary hardcoded user, later to be derived from auth token
export const USER_ID = process.env.SNOWBALL_BACKEND_USER_ID || '60f4355d-9549-4aac-9b54-eeefceeabef0'; export const USER_ID = process.env.SNOWBALL_BACKEND_USER_ID || '59f4355d-9549-4aac-9b54-eeefceeabef0';
export const PROJECT_DOMAIN = process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz'; export const PROJECT_DOMAIN = process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz';

View File

@ -14,6 +14,12 @@ import { ProjectMember } from './entity/ProjectMember';
import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { PROJECT_DOMAIN } from './constants'; import { PROJECT_DOMAIN } from './constants';
import { getEntities, loadAndSaveData } from './utils';
import { UserOrganization } from './entity/UserOrganization';
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
const USER_DATA_PATH = '../test/fixtures/users.json';
const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json';
const log = debug('snowball:database'); const log = debug('snowball:database');
@ -36,6 +42,25 @@ export class Database {
async init (): Promise<void> { async init (): Promise<void> {
await this.dataSource.initialize(); await this.dataSource.initialize();
log('database initialized'); log('database initialized');
const organizations = await this.getOrganizations({});
if (!organizations.length) {
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
// TODO: Remove user once authenticated
const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
const savedUsers = await loadAndSaveData(User, this.dataSource, [userEntities[0]]);
const userOrganizationRelations = {
member: savedUsers,
organization: savedOrgs
};
const userOrgEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
await loadAndSaveData(UserOrganization, this.dataSource, [userOrgEntities[0]], userOrganizationRelations);
}
} }
async getUser (options: FindOneOptions<User>): Promise<User | null> { async getUser (options: FindOneOptions<User>): Promise<User | null> {
@ -60,6 +85,13 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganizations (options: FindManyOptions<Organization>): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organizations = await organizationRepository.find(options);
return organizations;
}
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> { async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options); const organization = await organizationRepository.findOne(options);

View File

@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import toml from 'toml'; import toml from 'toml';
import debug from 'debug'; import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
const log = debug('snowball:utils'); const log = debug('snowball:utils');
@ -19,3 +20,44 @@ export const getConfig = async <ConfigType>(
return config; return config;
}; };
export const checkFileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
};
export const getEntities = async (filePath: string): Promise<any> => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
return entities;
};
export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, entities: any, relations?: any | undefined): Promise<Entity[]> => {
const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};

View File

@ -1,5 +1,4 @@
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; import { DataSource } from 'typeorm';
import * as fs from 'fs/promises';
import debug from 'debug'; import debug from 'debug';
import path from 'path'; import path from 'path';
@ -11,7 +10,7 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
import { Domain } from '../src/entity/Domain'; import { Domain } from '../src/entity/Domain';
import { ProjectMember } from '../src/entity/ProjectMember'; import { ProjectMember } from '../src/entity/ProjectMember';
import { Deployment } from '../src/entity/Deployment'; import { Deployment } from '../src/entity/Deployment';
import { getConfig } from '../src/utils'; import { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils';
import { Config } from '../src/config'; import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@ -27,43 +26,20 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'; const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
const entitiesData = await fs.readFile(filePath, 'utf-8');
const entities = JSON.parse(entitiesData);
const entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = [];
for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
if (relations) {
for (const field in relations) {
const valueIndex = String(field + 'Index');
entity = {
...entity,
[field]: relations[field][entityData[valueIndex]]
};
}
}
const dbEntity = await entityRepository.save(entity);
savedEntity.push(dbEntity);
}
return savedEntity;
};
const generateTestData = async (dataSource: DataSource) => { const generateTestData = async (dataSource: DataSource) => {
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities);
const projectRelations = { const projectRelations = {
owner: savedUsers, owner: savedUsers,
organization: savedOrgs organization: savedOrgs
}; };
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
const domainRepository = dataSource.getRepository(Domain); const domainRepository = dataSource.getRepository(Domain);
@ -71,14 +47,16 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects project: savedProjects
}; };
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations); const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations);
const domainRedirectedRelations = { const domainRedirectedRelations = {
project: savedProjects, project: savedProjects,
redirectTo: savedPrimaryDomains redirectTo: savedPrimaryDomains
}; };
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations); const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
const savedDomains = await domainRepository.find(); const savedDomains = await domainRepository.find();
@ -87,14 +65,16 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs organization: savedOrgs
}; };
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations); const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
const projectMemberRelations = { const projectMemberRelations = {
member: savedUsers, member: savedUsers,
project: savedProjects project: savedProjects
}; };
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations); const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
const deploymentRelations = { const deploymentRelations = {
project: savedProjects, project: savedProjects,
@ -102,23 +82,15 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers createdBy: savedUsers
}; };
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations); const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
const environmentVariableRelations = { const environmentVariableRelations = {
project: savedProjects project: savedProjects
}; };
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations); const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
}; await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
const checkFileExists = async (filePath: string) => {
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
log(err);
return false;
}
}; };
const main = async () => { const main = async () => {

View File

@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '', searchedBranch: '',
status: StatusOptions.ALL_STATUS, status: StatusOptions.ALL_STATUS,
}; };
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
const DeploymentsTabPanel = () => { const DeploymentsTabPanel = () => {
const client = useGQLClient(); const client = useGQLClient();
@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => {
const [deployments, setDeployments] = useState<Deployment[]>([]); const [deployments, setDeployments] = useState<Deployment[]>([]);
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]); const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
const fetchDeployments = async () => { const fetchDeployments = useCallback(async () => {
const { deployments } = await client.getDeployments(project.id); const { deployments } = await client.getDeployments(project.id);
setDeployments(deployments); setDeployments(deployments);
}; }, [client]);
const fetchProductionBranchDomains = useCallback(async () => { const fetchProductionBranchDomains = useCallback(async () => {
const { domains } = await client.getDomains(project.id, { const { domains } = await client.getDomains(project.id, {
branch: project.prodBranch, branch: project.prodBranch,
}); });
setProdBranchDomains(domains); setProdBranchDomains(domains);
}, []); }, [client]);
useEffect(() => { useEffect(() => {
fetchDeployments();
fetchProductionBranchDomains(); fetchProductionBranchDomains();
}, []); fetchDeployments();
const interval = setInterval(() => {
fetchDeployments();
}, FETCH_DEPLOYMENTS_INTERVAL);
return () => {
clearInterval(interval);
};
}, [fetchDeployments, fetchProductionBranchDomains]);
const currentDeployment = useMemo(() => { const currentDeployment = useMemo(() => {
return deployments.find((deployment) => { return deployments.find((deployment) => {