Compare commits

..
Author SHA1 Message Date
Vivian Phung 8038744dc1 feat: access code 2024-06-22 01:08:54 +00:00
Vivian Phung 934aa1a26b Refactor: Env to utils/constants (#210)
This PR centralizes all the environment variable references into a single constants file. The change includes replacing various `import.meta.env` references with imports from the new `utils/constants` module. This improves maintainability by providing a single place to manage environment variables.
2024-06-21 21:07:41 -04:00
Gilbert d975390a1b Fix url env var 2024-06-20 14:54:17 -05:00
Gilbert f77323364c Bump 2024-06-20 04:06:16 -05:00
Gilbert c6a78f2116 Just send back the message, this is a demo after all 2024-06-20 04:05:48 -05:00
Vivian Phung cff9a5b2ea [nit] DeploymentMenu dependencies cleanup (#204)
### TL;DR

This PR updates the project settings.

### What changed?

The project settings have been refactored for better organization and readability.

### How to test?

To test this change, navigate to the project settings and ensure all options are functioning as expected.

### Why make this change?

This change was made to improve the user experience when navigating through the project settings.

---
2024-06-20 00:40:32 -04:00
Vivian Phung 003b83ba21 Refactor: DeploymentMenu uses toast component (#203)
### TL;DR

This PR includes updates to the project settings.

### What changed?

The project settings have been refactored for better usability and consistency with other components.

### How to test?

To test this change, navigate to the project settings and ensure all options are working as expected.

### Why make this change?

This change was made to improve the user experience and maintain consistency across the application.

---
2024-06-20 00:36:43 -04:00
Vivian Phung 82a1c151a8 Refactor: AssignDomainDialog uses Modal component (#202)
### TL;DR

This PR includes updates to the project settings.

### What changed?

The project settings have been refactored for better organization and readability.

### How to test?

To test this change, navigate to the project settings and ensure all options are functioning as expected.

### Why make this change?

This change was made to improve the user experience when navigating through the project settings.

---
2024-06-20 00:32:41 -04:00
Gilbert 2ada11f311 Support async express handlers 2024-06-19 22:59:20 -05:00
Gilbert 6e32d0678a Add generic error handling 2024-06-19 22:31:07 -05:00
Gilbert 198478f5fa Add logs for debugging 2024-06-19 14:22:55 -05:00
dboreham 552dfe783e Merge pull request #209 from snowball-tools/dboreham/add-rpid-config
Add missing rpid env var
2024-06-18 20:04:19 -06:00
Gilbert e2bf5d052c Bump server version 2024-06-18 17:46:47 -05:00
dboreham 33323b9bbf Merge pull request #208 from snowball-tools/dboreham/error-checked-deployment
Make deployment scripts more robust
2024-06-18 13:51:33 -06:00
dboreham e2a3254563 Add missing rpid env var 2024-06-18 13:47:26 -06:00
dboreham 8589bf4094 Make deployment scripts more robust 2024-06-18 11:15:31 -06:00
dboreham f8ebdfe7aa Merge pull request #207 from snowball-tools/telackey-patch-1
Update config.staging.yml
2024-06-18 06:31:58 -06:00
telackey 2166b2f800 Update config.staging.yml 2024-06-17 23:41:01 -05:00
dboreham cdd8d15e73 Increase gas for staging deployment chain profile to match prod (#205) 2024-06-17 09:49:52 +05:30
Vivian Phung dc7b251988 add version backend (temp) (#201)
* add version backend (temp)

* staging router endpoint

* remove frontend
2024-06-05 17:38:19 +01:00
Gilbert bfb4a3f30b Revert "remove email signup for now"
This reverts commit 0e9c3a07fd.
2024-06-04 16:26:15 -05:00
Gilbert 8f7fc888a9 Log commit hash 2024-06-04 15:56:52 -05:00
Vivian Phung 0e9c3a07fd remove email signup for now 2024-06-04 15:56:52 -05:00
Vivian Phung 61e3e88a6c Refactor Input and SearchBar (#199)
### TL;DR

This pull request refactors the `SearchBar` and `Input` components, adding a `ref` to the former and removing an incorrect understanding of `react-hook-form` (yes, i prev "fix" the component) in the latter.

### What changed?

A ref is added to the SearchBar component for better control and handling. In the Input component, we have eliminated the usage of 'react-hook-form' and as a result, the 'register' prop is removed. This makes the component less reliant on specific libraries and more reusable.

### How to test?

Ensure that proper testing is done on the updated components. Make sure that the `SearchBar` works as expected with its ref and that Input does not depend on 'react-hook-form' anymore.

### Why make this change?

This change was made to improve the functionality of the `SearchBar` and the flexibility of the Input component, making them more effective and reusable respectively. The changes also align with the current code quality standards and best practices.
2024-05-22 15:06:50 -04:00
Vivian Phung 7b5ba1a5d0 correct suppressRefError (#198)
### TL;DR

This PR refactors the `UserSelect` component, adjusting the call to `getToggleButtonProps`.

### What changed?

The `getToggleButtonProps` method in the `UserSelect` component now takes in two separate objects, one for the `ref` and another for `suppressRefError`, instead of a single one.

### How to test?

Verify the component functionality hasn't changed and there are no reference errors.

### Why make this change?

This code changes improve the readability and maintainability of this component by clearly separating the component reference and error suppression configurations in separate objects.
2024-05-22 15:02:51 -04:00
Vivian Phung b35f4033c5 Refactor: Collaborator Project Settings (#197)
### TL;DR

AddMemberDialog component now uses a Select dropdown for permissions instead of Checkboxes. CollaboratorsTabPanel now includes dismiss functionality for toasts.

### What changed?

- Updated AddMemberDialog to use a Select dropdown for permissions
- Added dismiss functionality for toasts in CollaboratorsTabPanel

### How to test?

Test the functionality of selecting permissions using the dropdown and toast dismissal in CollaboratorsTabPanel.

### Why make this change?

To improve user experience and UI consistency in permissions selection and toast management.
2024-05-22 14:58:47 -04:00
38 changed files with 935 additions and 474 deletions
+1
View File
@@ -19,6 +19,7 @@
"cors": "^2.8.5",
"debug": "^4.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"express-session": "^1.18.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
+1
View File
@@ -1,3 +1,4 @@
import 'express-async-errors';
import 'reflect-metadata';
import debug from 'debug';
import fs from 'fs';
+56 -38
View File
@@ -25,8 +25,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId);
},
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.user, organizationSlug);
projectsInOrganization: async (
_: any,
{ organizationSlug }: { organizationSlug: string },
context: any,
) => {
return service.getProjectsInOrganization(
context.user,
organizationSlug,
);
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
@@ -35,7 +42,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
{ projectId }: { projectId: string },
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -44,7 +51,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId);
},
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
searchProjects: async (
_: any,
{ searchText }: { searchText: string },
context: any,
) => {
return service.searchProjects(context.user, searchText);
},
@@ -52,11 +63,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
filter,
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
) => {
return service.getDomainsByProjectId(projectId, filter);
}
},
},
// TODO: Return error in GQL response
@@ -64,10 +75,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
context: any,
) => {
try {
return await service.removeProjectMember(context.user, projectMemberId);
return await service.removeProjectMember(
context.user,
projectMemberId,
);
} catch (err) {
log(err);
return false;
@@ -78,13 +92,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectMemberId,
data
data,
}: {
projectMemberId: string;
data: {
permissions: Permission[];
};
}
},
) => {
try {
return await service.updateProjectMember(projectMemberId, data);
@@ -98,14 +112,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data
data,
}: {
projectId: string;
data: {
email: string;
permissions: Permission[];
};
}
},
) => {
try {
return Boolean(await service.addProjectMember(projectId, data));
@@ -119,15 +133,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data
data,
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
}
},
) => {
try {
return Boolean(
await service.addEnvironmentVariables(projectId, data)
await service.addEnvironmentVariables(projectId, data),
);
} catch (err) {
log(err);
@@ -139,16 +153,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
environmentVariableId,
data
data,
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
},
) => {
try {
return await service.updateEnvironmentVariable(
environmentVariableId,
data
data,
);
} catch (err) {
log(err);
@@ -158,7 +172,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
{ environmentVariableId }: { environmentVariableId: string },
) => {
try {
return await service.removeEnvironmentVariable(environmentVariableId);
@@ -171,10 +185,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
context: any,
) => {
try {
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
return Boolean(
await service.updateDeploymentToProd(context.user, deploymentId),
);
} catch (err) {
log(err);
return false;
@@ -185,9 +201,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
organizationSlug,
data
data,
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any
context: any,
) => {
try {
return await service.addProject(context.user, organizationSlug, data);
@@ -199,7 +215,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
) => {
try {
return await service.updateProject(projectId, data);
@@ -212,10 +228,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any
context: any,
) => {
try {
return Boolean(await service.redeployToProd(context.user, deploymentId));
return Boolean(
await service.redeployToProd(context.user, deploymentId),
);
} catch (err) {
log(err);
return false;
@@ -244,8 +262,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
deploymentId
}: { deploymentId: string; projectId: string }
deploymentId,
}: { deploymentId: string; projectId: string },
) => {
try {
return await service.rollbackDeployment(projectId, deploymentId);
@@ -257,9 +275,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
deleteDeployment: async (
_: any,
{
deploymentId
}: { deploymentId: string; }
{ deploymentId }: { deploymentId: string },
) => {
try {
return await service.deleteDeployment(deploymentId);
@@ -271,7 +287,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } }
{ projectId, data }: { projectId: string; data: { name: string } },
) => {
try {
return Boolean(await service.addDomain(projectId, data));
@@ -283,7 +299,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
) => {
try {
return await service.updateDomain(domainId, data);
@@ -296,7 +312,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any
context: any,
) => {
try {
return await service.authenticateGitHub(code, context.user);
@@ -308,12 +324,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => {
try {
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
return service.unauthenticateGitHub(context.user, {
gitHubToken: null,
});
} catch (err) {
log(err);
return false;
}
}
}
},
},
};
};
+12
View File
@@ -5,6 +5,16 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '444444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
//
// Turnkey
//
@@ -19,6 +29,7 @@ router.get('/registration/:email', async (req, res) => {
});
router.post('/register', async (req, res) => {
console.log('Register', req.body);
const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), {
challenge,
@@ -31,6 +42,7 @@ router.post('/register', async (req, res) => {
});
router.post('/authenticate', async (req, res) => {
console.log('Authenticate', req.body);
const { signedWhoamiRequest } = req.body;
const user = await authenticateUser(
req.app.get('service'),
+9
View File
@@ -0,0 +1,9 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.7' });
});
export default router;
+7
View File
@@ -17,6 +17,7 @@ import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth';
import stagingRouter from './routes/staging';
import { Service } from './service';
const log = debug('snowball:server');
@@ -109,6 +110,12 @@ export const createAndStartServer = async (
app.set('service', service);
app.use('/auth', authRouter);
app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);
res.status(500).json({ error: err.message });
});
httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
+225 -169
View File
@@ -15,7 +15,12 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
import { User } from './entity/User';
import { Registry } from './registry';
import { GitHubConfig, RegistryConfig } from './config';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types';
import {
AppDeploymentRecord,
AppDeploymentRemovalRecord,
GitPushEventPayload,
PackageJSON,
} from './types';
import { Role } from './entity/UserOrganization';
const log = debug('snowball:service');
@@ -71,33 +76,32 @@ export class Service {
// Fetch deployments in building state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Building
}
status: DeploymentStatus.Building,
},
});
if (deployments.length) {
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`,
);
// Calculate a timestamp for one hour ago
const anHourAgo = Date.now() - HOUR;
// Filter out deployments started more than an hour ago and mark them as Error
const oldDeploymentsToUpdate = deployments.filter(
deployment => (Number(deployment.updatedAt) < anHourAgo)
)
const oldDeploymentsToUpdate = deployments
.filter((deployment) => Number(deployment.updatedAt) < anHourAgo)
.map((deployment) => {
return this.db.updateDeploymentById(deployment.id, {
status: DeploymentStatus.Error,
isCurrent: false
isCurrent: false,
});
});
// If there are old deployments to update, log and perform the updates
if (oldDeploymentsToUpdate.length > 0) {
log(
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`,
);
await Promise.all(oldDeploymentsToUpdate);
}
@@ -125,17 +129,18 @@ export class Service {
// Fetch deployments in deleting state
const deployments = await this.db.getDeployments({
where: {
status: DeploymentStatus.Deleting
}
status: DeploymentStatus.Deleting,
},
});
if (deployments.length) {
log(
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`,
);
// Fetch ApplicationDeploymentRemovalRecords for deployments
const records = await this.registry.getDeploymentRemovalRecords(deployments);
const records =
await this.registry.getDeploymentRemovalRecords(deployments);
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
@@ -153,16 +158,16 @@ export class Service {
* Update deployments with ApplicationDeploymentRecord data
*/
async updateDeploymentsWithRecordData(
records: AppDeploymentRecord[]
records: AppDeploymentRecord[],
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({
where: records.map((record) => ({
applicationRecordId: record.attributes.application
applicationRecordId: record.attributes.application,
})),
order: {
createdAt: 'DESC'
}
createdAt: 'DESC',
},
});
// Get project IDs of deployments that are in production environment
@@ -174,13 +179,13 @@ export class Service {
return acc;
},
new Set<string>()
new Set<string>(),
);
// Set old deployments isCurrent to false
await this.db.updateDeploymentsByProjectIds(
Array.from(productionDeploymentProjectIds),
{ isCurrent: false }
{ isCurrent: false },
);
const recordToDeploymentsMap = deployments.reduce(
@@ -188,7 +193,7 @@ export class Service {
acc[deployment.applicationRecordId] = deployment;
return acc;
},
{}
{},
);
// Update deployment data for ApplicationDeploymentRecords
@@ -200,11 +205,11 @@ export class Service {
applicationDeploymentRecordData: record.attributes,
url: record.attributes.url,
status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production
isCurrent: deployment.environment === Environment.Production,
});
log(
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
);
});
@@ -218,17 +223,23 @@ export class Service {
records: AppDeploymentRemovalRecord[],
deployments: Deployment[],
): Promise<void> {
const removedApplicationDeploymentRecordIds = records.map(record => record.attributes.deployment);
const removedApplicationDeploymentRecordIds = records.map(
(record) => record.attributes.deployment,
);
// Get removed deployments for ApplicationDeploymentRecords
const removedDeployments = deployments.filter(deployment => removedApplicationDeploymentRecordIds.includes(deployment.applicationDeploymentRecordId!))
const removedDeployments = deployments.filter((deployment) =>
removedApplicationDeploymentRecordIds.includes(
deployment.applicationDeploymentRecordId!,
),
);
const recordToDeploymentsMap = removedDeployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationDeploymentRecordId!] = deployment;
return acc;
},
{}
{},
);
// Update deployment data for ApplicationDeploymentRecords and delete
@@ -241,10 +252,10 @@ export class Service {
});
log(
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`,
);
await this.db.deleteDeploymentById(deployment.id)
await this.db.deleteDeploymentById(deployment.id);
});
await Promise.all(deploymentUpdatePromises);
@@ -253,41 +264,41 @@ export class Service {
async getUser(userId: string): Promise<User | null> {
return this.db.getUser({
where: {
id: userId
}
id: userId,
},
});
}
async getUserByEmail(email: string): Promise<User | null> {
return await this.db.getUser({
where: {
email
}
email,
},
});
}
async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
return await this.db.getUser({
where: {
subOrgId
}
subOrgId,
},
});
}
async getUserByEthAddress(ethAddress: string): Promise<User | null> {
return await this.db.getUser({
where: {
ethAddress
}
ethAddress,
},
});
}
async createUser(params: {
name: string
email: string
subOrgId: string
ethAddress: string
turnkeyWalletId: string
name: string;
email: string;
subOrgId: string;
ethAddress: string;
turnkeyWalletId: string;
}): Promise<User> {
const [org] = await this.db.getOrganizations({});
assert(org, 'No organizations exists in database');
@@ -305,7 +316,7 @@ export class Service {
await this.db.addUserOrganization({
member: user,
organization: org,
role: Role.Owner
role: Role.Owner,
});
return user;
@@ -315,7 +326,7 @@ export class Service {
const user = await this.db.getUser({ where: { id: userId } });
assert(
user && user.gitHubToken,
'User needs to be authenticated with GitHub token'
'User needs to be authenticated with GitHub token',
);
return new Octokit({ auth: user.gitHubToken });
@@ -331,8 +342,14 @@ export class Service {
return dbProject;
}
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
async getProjectsInOrganization(
user: User,
organizationSlug: string,
): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(
user.id,
organizationSlug,
);
return dbProjects;
}
@@ -342,7 +359,7 @@ export class Service {
}
async getEnvironmentVariablesByProjectId(
projectId: string
projectId: string,
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
@@ -350,7 +367,7 @@ export class Service {
}
async getProjectMembersByProjectId(
projectId: string
projectId: string,
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
@@ -358,13 +375,16 @@ export class Service {
}
async searchProjects(user: User, searchText: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
const dbProjects = await this.db.getProjectsBySearchText(
user.id,
searchText,
);
return dbProjects;
}
async getDomainsByProjectId(
projectId: string,
filter?: FindOptionsWhere<Domain>
filter?: FindOptionsWhere<Domain>,
): Promise<Domain[]> {
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains;
@@ -372,7 +392,7 @@ export class Service {
async updateProjectMember(
projectMemberId: string,
data: { permissions: Permission[] }
data: { permissions: Permission[] },
): Promise<boolean> {
return this.db.updateProjectMemberById(projectMemberId, data);
}
@@ -382,36 +402,39 @@ export class Service {
data: {
email: string;
permissions: Permission[];
}
},
): Promise<ProjectMember> {
// TODO: Send invitation
let user = await this.db.getUser({
where: {
email: data.email
}
email: data.email,
},
});
if (!user) {
user = await this.db.addUser({
email: data.email
email: data.email,
});
}
const newProjectMember = await this.db.addProjectMember({
project: {
id: projectId
id: projectId,
},
permissions: data.permissions,
isPending: true,
member: {
id: user.id
}
id: user.id,
},
});
return newProjectMember;
}
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
async removeProjectMember(
user: User,
projectMemberId: string,
): Promise<boolean> {
const member = await this.db.getProjectMemberById(projectMemberId);
if (String(member.member.id) === user.id) {
@@ -430,7 +453,7 @@ export class Service {
async addEnvironmentVariables(
projectId: string,
data: { environments: string[]; key: string; value: string }[]
data: { environments: string[]; key: string; value: string }[],
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
@@ -440,38 +463,41 @@ export class Service {
value: environmentVariable.value,
environment: environment as Environment,
project: Object.assign(new Project(), {
id: projectId
})
id: projectId,
}),
};
});
})
.flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables
formattedEnvironmentVariables,
);
return savedEnvironmentVariables;
}
async updateEnvironmentVariable(
environmentVariableId: string,
data: DeepPartial<EnvironmentVariable>
data: DeepPartial<EnvironmentVariable>,
): Promise<boolean> {
return this.db.updateEnvironmentVariable(environmentVariableId, data);
}
async removeEnvironmentVariable(
environmentVariableId: string
environmentVariableId: string,
): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId);
}
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
async updateDeploymentToProd(
user: User,
deploymentId: string,
): Promise<Deployment> {
const oldDeployment = await this.db.getDeployment({
where: { id: deploymentId },
relations: {
project: true
}
project: true,
},
});
if (!oldDeployment) {
@@ -480,20 +506,18 @@ export class Service {
const prodBranchDomains = await this.db.getDomainsByProjectId(
oldDeployment.project.id,
{ branch: oldDeployment.project.prodBranch }
{ branch: oldDeployment.project.prodBranch },
);
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id,
octokit,
{
const newDeployment = await this.createDeployment(user.id, octokit, {
project: oldDeployment.project,
branch: oldDeployment.branch,
environment: Environment.Production,
domain: prodBranchDomains[0],
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
commitMessage: oldDeployment.commitMessage,
});
return newDeployment;
@@ -502,11 +526,11 @@ export class Service {
async createDeployment(
userId: string,
octokit: Octokit,
data: DeepPartial<Deployment>
data: DeepPartial<Deployment>,
): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found');
log(
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
`Creating deployment in project ${data.project.name} from branch ${data.branch}`,
);
const [owner, repo] = data.project.repository.split('/');
@@ -514,7 +538,7 @@ export class Service {
owner,
repo,
path: 'package.json',
ref: data.commitHash
ref: data.commitHash,
});
if (!packageJSONData) {
@@ -526,10 +550,12 @@ export class Service {
assert(packageJSON.name, "name field doesn't exist in package.json");
const repoUrl = (await octokit.rest.repos.get({
const repoUrl = (
await octokit.rest.repos.get({
owner,
repo
})).data.html_url;
repo,
})
).data.html_url;
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } =
@@ -538,17 +564,20 @@ export class Service {
packageJSON,
appType: data.project!.template!,
commitHash: data.commitHash!,
repoUrl
repoUrl,
});
// Update previous deployment with prod branch domain
// TODO: Fix unique constraint error for domain
if (data.domain) {
await this.db.updateDeployment({
domainId: data.domain.id
}, {
domain: null
});
await this.db.updateDeployment(
{
domainId: data.domain.id,
},
{
domain: null,
},
);
}
const newDeployment = await this.db.addDeployment({
@@ -562,53 +591,67 @@ export class Service {
applicationRecordData,
domain: data.domain,
createdBy: Object.assign(new User(), {
id: userId
})
id: userId,
}),
});
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
log(
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
);
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
const environmentVariables =
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
environment: Environment.Production,
});
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
const environmentVariablesObj = environmentVariables.reduce(
(acc, env) => {
acc[env.key] = env.value;
return acc;
}, {} as { [key: string]: string });
},
{} as { [key: string]: string },
);
// To set project DNS
if (data.environment === Environment.Production) {
// On deleting deployment later, project DNS deployment is also deleted
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
await this.registry.createApplicationDeploymentRequest(
{
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}`
dns: `${newDeployment.project.name}`,
});
}
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
{
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
await this.registry.createApplicationDeploymentRequest({
deployment: newDeployment,
appName: repo,
repository: repoUrl,
environmentVariables: environmentVariablesObj,
dns: `${newDeployment.project.name}-${newDeployment.id}`
dns: `${newDeployment.project.name}-${newDeployment.id}`,
});
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
await this.db.updateDeploymentById(newDeployment.id, {
applicationDeploymentRequestId,
applicationDeploymentRequestData,
});
return newDeployment;
}
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
async addProject(
user: User,
organizationSlug: string,
data: DeepPartial<Project>,
): Promise<Project | undefined> {
const organization = await this.db.getOrganization({
where: {
slug: organizationSlug
}
slug: organizationSlug,
},
});
if (!organization) {
throw new Error('Organization does not exist');
@@ -620,26 +663,23 @@ export class Service {
const [owner, repo] = project.repository.split('/');
const {
data: [latestCommit]
data: [latestCommit],
} = await octokit.rest.repos.listCommits({
owner,
repo,
sha: project.prodBranch,
per_page: 1
per_page: 1,
});
// Create deployment with prod branch and latest commit
await this.createDeployment(user.id,
octokit,
{
await this.createDeployment(user.id, octokit, {
project,
branch: project.prodBranch,
environment: Environment.Production,
domain: null,
commitHash: latestCommit.sha,
commitMessage: latestCommit.commit.message
}
);
commitMessage: latestCommit.commit.message,
});
await this.createRepoHook(octokit, project);
@@ -655,11 +695,11 @@ export class Service {
config: {
url: new URL(
'api/github/webhook',
this.config.gitHubConfig.webhookUrl
this.config.gitHubConfig.webhookUrl,
).href,
content_type: 'json'
content_type: 'json',
},
events: ['push']
events: ['push'],
});
} catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
@@ -668,7 +708,7 @@ export class Service {
err instanceof RequestError &&
err.status === 422 &&
(err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR,
)
)
) {
@@ -687,9 +727,11 @@ export class Service {
return;
}
log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
log(
`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`,
);
const projects = await this.db.getProjects({
where: { repository: repository.full_name }
where: { repository: repository.full_name },
});
if (!projects.length) {
@@ -703,7 +745,7 @@ export class Service {
for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId);
const [domain] = await this.db.getDomainsByProjectId(project.id, {
branch
branch,
});
// Create deployment with branch and latest commit in GitHub data
@@ -716,14 +758,14 @@ export class Service {
: Environment.Preview,
domain,
commitHash: headCommit.id,
commitMessage: headCommit.message
commitMessage: headCommit.message,
});
}
}
async updateProject(
projectId: string,
data: DeepPartial<Project>
data: DeepPartial<Project>,
): Promise<boolean> {
return this.db.updateProjectById(projectId, data);
}
@@ -736,13 +778,13 @@ export class Service {
async deleteDomain(domainId: string): Promise<boolean> {
const domainsRedirectedFrom = await this.db.getDomains({
where: {
redirectToId: domainId
}
redirectToId: domainId,
},
});
if (domainsRedirectedFrom.length > 0) {
throw new Error(
'Cannot delete domain since it has redirects from other domains'
'Cannot delete domain since it has redirects from other domains',
);
}
@@ -754,11 +796,11 @@ export class Service {
relations: {
project: true,
domain: true,
createdBy: true
createdBy: true,
},
where: {
id: deploymentId
}
id: deploymentId,
},
});
if (oldDeployment === null) {
@@ -767,16 +809,14 @@ export class Service {
const octokit = await this.getOctokit(user.id);
const newDeployment = await this.createDeployment(user.id,
octokit,
{
const newDeployment = await this.createDeployment(user.id, octokit, {
project: oldDeployment.project,
// TODO: Put isCurrent field in project
branch: oldDeployment.branch,
environment: Environment.Production,
domain: oldDeployment.domain,
commitHash: oldDeployment.commitHash,
commitMessage: oldDeployment.commitMessage
commitMessage: oldDeployment.commitMessage,
});
return newDeployment;
@@ -784,19 +824,19 @@ export class Service {
async rollbackDeployment(
projectId: string,
deploymentId: string
deploymentId: string,
): Promise<boolean> {
// TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({
relations: {
domain: true
domain: true,
},
where: {
project: {
id: projectId
id: projectId,
},
isCurrent: true,
},
isCurrent: true
}
});
if (!oldCurrentDeployment) {
@@ -805,12 +845,12 @@ export class Service {
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
oldCurrentDeployment.id,
{ isCurrent: false, domain: null }
{ isCurrent: false, domain: null },
);
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
deploymentId,
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
{ isCurrent: true, domain: oldCurrentDeployment?.domain },
);
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
@@ -819,11 +859,11 @@ export class Service {
async deleteDeployment(deploymentId: string): Promise<boolean> {
const deployment = await this.db.getDeployment({
where: {
id: deploymentId
id: deploymentId,
},
relations: {
project: true
}
project: true,
},
});
if (deployment && deployment.applicationDeploymentRecordId) {
@@ -831,31 +871,39 @@ export class Service {
if (deployment.isCurrent) {
const currentDeploymentURL = `https://${deployment.project.subDomain}`;
const deploymentRecords = await this.registry.getDeploymentRecordsByFilter({
const deploymentRecords =
await this.registry.getDeploymentRecordsByFilter({
application: deployment.applicationRecordId,
url: currentDeploymentURL
})
url: currentDeploymentURL,
});
if (!deploymentRecords.length) {
log(`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`);
log(
`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`,
);
return false;
}
await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deploymentRecords[0].id });
await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deploymentRecords[0].id,
});
}
const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId });
const result =
await this.registry.createApplicationDeploymentRemovalRequest({
deploymentId: deployment.applicationDeploymentRecordId,
});
await this.db.updateDeploymentById(
deployment.id,
{
await this.db.updateDeploymentById(deployment.id, {
status: DeploymentStatus.Deleting,
applicationDeploymentRemovalRequestId: result.applicationDeploymentRemovalRequestId,
applicationDeploymentRemovalRequestData: result.applicationDeploymentRemovalRequestData
}
);
applicationDeploymentRemovalRequestId:
result.applicationDeploymentRemovalRequestId,
applicationDeploymentRemovalRequestData:
result.applicationDeploymentRemovalRequestData,
});
return (result !== undefined || result !== null);
return result !== undefined || result !== null;
}
return false;
@@ -863,7 +911,7 @@ export class Service {
async addDomain(
projectId: string,
data: { name: string }
data: { name: string },
): Promise<{
primaryDomain: Domain;
redirectedDomain: Domain;
@@ -877,7 +925,7 @@ export class Service {
const primaryDomainDetails = {
...data,
branch: currentProject.prodBranch,
project: currentProject
project: currentProject,
};
const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails);
@@ -888,27 +936,27 @@ export class Service {
name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`,
branch: currentProject.prodBranch,
project: currentProject,
redirectTo: savedPrimaryDomain
redirectTo: savedPrimaryDomain,
};
const savedRedirectedDomain = await this.db.addDomain(
redirectedDomainDetails
redirectedDomainDetails,
);
return {
primaryDomain: savedPrimaryDomain,
redirectedDomain: savedRedirectedDomain
redirectedDomain: savedRedirectedDomain,
};
}
async updateDomain(
domainId: string,
data: DeepPartial<Domain>
data: DeepPartial<Domain>,
): Promise<boolean> {
const domain = await this.db.getDomain({
where: {
id: domainId
}
id: domainId,
},
});
if (domain === null) {
@@ -916,16 +964,16 @@ export class Service {
}
const newDomain = {
...data
...data,
};
const domainsRedirectedFrom = await this.db.getDomains({
where: {
project: {
id: domain.projectId
id: domain.projectId,
},
redirectToId: domain.id,
},
redirectToId: domain.id
}
});
// If there are domains redirecting to current domain, only branch of current domain can be updated
@@ -936,8 +984,8 @@ export class Service {
if (data.redirectToId) {
const redirectedDomain = await this.db.getDomain({
where: {
id: data.redirectToId
}
id: data.redirectToId,
},
});
if (redirectedDomain === null) {
@@ -946,7 +994,7 @@ export class Service {
if (redirectedDomain.redirectToId) {
throw new Error(
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.',
);
}
@@ -958,9 +1006,14 @@ export class Service {
return updateResult;
}
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
const { authentication: { token } } = await this.oauthApp.createToken({
code
async authenticateGitHub(
code: string,
user: User,
): Promise<{ token: string }> {
const {
authentication: { token },
} = await this.oauthApp.createToken({
code,
});
await this.db.updateUser(user, { gitHubToken: token });
@@ -968,7 +1021,10 @@ export class Service {
return { token };
}
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
async unauthenticateGitHub(
user: User,
data: DeepPartial<User>,
): Promise<boolean> {
return this.db.updateUser(user, data);
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ services:
restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
chainId: laconic_9000-1
gas: 550000
gas: 1200000
fees: 200000aphoton
+35 -3
View File
@@ -64,7 +64,13 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
@@ -73,15 +79,36 @@ REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -90,6 +117,11 @@ fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+37 -5
View File
@@ -44,8 +44,8 @@ record:
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -70,7 +70,13 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
@@ -79,15 +85,36 @@ REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
exit $rc
fi
sleep 2
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -96,6 +123,11 @@ fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+3 -2
View File
@@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug';
@@ -10,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages';
import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout';
import { useEffect } from 'react';
import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([
{
@@ -59,7 +60,7 @@ function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
fetch(`${baseUrl}/auth/session`, {
credentials: 'include',
}).then((res) => {
if (res.status !== 200) {
+7 -2
View File
@@ -1,16 +1,21 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from 'utils/constants';
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
@@ -6,7 +6,7 @@ import { Input, InputProps } from './shared/Input';
const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement,
InputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
return (
<div className="relative flex w-full">
<Input
@@ -18,6 +18,7 @@ const SearchBar: React.ForwardRefRenderFunction<
appearance="borderless"
className="w-full lg:w-[459px]"
{...props}
ref={ref}
/>
</div>
);
@@ -14,11 +14,10 @@ import { useToast } from '../../shared/Toast';
import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface {
onAuth: (token: string) => void;
@@ -1,13 +1,8 @@
import { CopyBlock, atomOneLight } from 'react-code-blocks';
import { Link } from 'react-router-dom';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
} from '@snowballtools/material-tailwind-react-fork';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
interface AssignDomainProps {
open: boolean;
@@ -16,11 +11,12 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
In order to assign a domain to your production deployments, configure it
in the{' '}
<Modal open={open} onOpenChange={handleOpen}>
<Modal.Content>
<Modal.Header>Assign Domain</Modal.Header>
<Modal.Body>
In order to assign a domain to your production deployments, configure
it in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '}
@@ -34,18 +30,12 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
showLineNumbers={false}
theme={atomOneLight}
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button
className="rounded-3xl"
variant="gradient"
color="blue"
onClick={handleOpen}
>
<span>Okay</span>
</Button>
</DialogFooter>
</Dialog>
</Modal.Body>
<Modal.Footer className="flex justify-start">
<Button onClick={handleOpen}>Okay</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
);
};
@@ -1,5 +1,12 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useState, ComponentPropsWithRef } from 'react';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { Deployment, Domain, Environment, Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import {
@@ -11,17 +18,11 @@ import {
UndoIcon,
CrossCircleIcon,
} from 'components/shared/CustomIcon';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { ComponentPropsWithRef } from 'react';
import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { useToast } from 'components/shared/Toast';
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
deployment: Deployment;
@@ -41,6 +42,7 @@ export const DeploymentMenu = ({
...props
}: DeploymentMenuProps) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false);
@@ -51,9 +53,19 @@ export const DeploymentMenu = ({
const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) {
await onUpdate();
toast.success('Deployment changed to production');
toast({
id: 'deployment_changed_to_production',
title: 'Deployment changed to production',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to change deployment to production');
toast({
id: 'deployment_not_changed_to_production',
title: 'Error changing deployment to production',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -61,9 +73,19 @@ export const DeploymentMenu = ({
const isRedeployed = await client.redeployToProd(deployment.id);
if (isRedeployed) {
await onUpdate();
toast.success('Redeployed to production');
toast({
id: 'redeployed_to_production',
title: 'Redeployed to production',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to redeploy to production');
toast({
id: 'redeployed_to_production_failed',
title: 'Error redeploying to production',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -74,9 +96,19 @@ export const DeploymentMenu = ({
);
if (isRollbacked) {
await onUpdate();
toast.success('Deployment rolled back');
toast({
id: 'deployment_rolled_back',
title: 'Deployment rolled back',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to rollback deployment');
toast({
id: 'deployment_rollback_failed',
title: 'Error rolling back deployment',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -84,9 +116,19 @@ export const DeploymentMenu = ({
const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) {
await onUpdate();
toast.success('Deleted deployment');
toast({
id: 'deployment_deleted',
title: 'Deployment deleted',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast.error('Unable to delete deployment');
toast({
id: 'deployment_not_deleted',
title: 'Error deleting deployment',
variant: 'error',
onDismiss: dismiss,
});
}
};
@@ -4,7 +4,6 @@ import {
useMemo,
ComponentPropsWithoutRef,
} from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
@@ -12,7 +11,7 @@ import { cn } from 'utils/classnames';
import { InputTheme, inputTheme } from './Input.theme';
export interface InputProps<T extends FieldValues = FieldValues>
export interface InputProps
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
@@ -20,9 +19,6 @@ export interface InputProps<T extends FieldValues = FieldValues>
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
// react-hook-form optional register
register?: ReturnType<UseFormRegister<T>>;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
@@ -34,7 +30,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
leftIcon,
rightIcon,
helperText,
register,
size,
state,
appearance,
@@ -107,12 +102,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
{...(register ? register : {})}
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
{...props}
ref={ref}
{...props}
/>
{rightIcon && renderRightIcon}
</div>
@@ -20,6 +20,7 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants';
interface SidebarProps {
mobileOpen?: boolean;
@@ -83,7 +84,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]);
const handleLogOut = useCallback(async () => {
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
await fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
});
@@ -103,10 +103,12 @@ export const UserSelect = ({ options, value }: UserSelectProps) => {
<div className={theme.container()}>
{/* Input */}
<div
{...getToggleButtonProps({
{...getToggleButtonProps(
{
ref: inputWrapperRef,
suppressRefError: true,
})}
},
{ suppressRefError: true },
)}
onClick={() => !dropdownOpen && openMenu()}
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
>
@@ -1,9 +1,11 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient();
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
if (!VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
}
+6 -2
View File
@@ -14,13 +14,17 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants';
// @ts-ignore
console.log(`v-${__VERSION__}`);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint });
@@ -0,0 +1,94 @@
import React, { useState } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
LoaderIcon,
} from 'components/shared/CustomIcon';
import { WavyBorder } from 'components/shared/WavyBorder';
import { Input } from 'components/shared/Input';
import { verifyAccessCode } from 'utils/accessCode';
type AccessMethod = 'accesscode' | 'passkey';
type Err = { type: AccessMethod; message: string };
type AccessCodeProps = {
onCorrectAccessCode: () => void;
};
export const AccessCode: React.FC<AccessCodeProps> = ({
onCorrectAccessCode,
}) => {
const [accessCode, setAccessCode] = useState('');
const [error, setError] = useState<Err | null>();
const [accessMethod, setAccessMethod] = useState<AccessMethod | false>(false);
async function validateAccessCode() {
setAccessMethod('accesscode');
try {
const isValidAccessCode = await verifyAccessCode(accessCode);
if (isValidAccessCode) {
localStorage.setItem('accessCode', accessCode);
onCorrectAccessCode();
} else {
setError({
type: 'accesscode',
message: 'Invalid access code',
});
}
} catch (err: any) {
setError({ type: 'accesscode', message: err.message });
}
}
const loading = accessMethod;
const isValidAccessCodeLength = accessCode.length === 6;
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
Access Code
</div>
</div>
<WavyBorder className="self-stretch" variant="stroke" />
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
<div className="self-stretch flex-col gap-8 flex">
<div className="flex-col justify-start items-start gap-2 inline-flex">
<Input
value={accessCode}
onChange={(e) => setAccessCode(e.target.value)}
disabled={!!loading}
/>
</div>
<Button
rightIcon={
loading && loading === 'accesscode' ? (
<LoaderIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon height="16" />
)
}
onClick={() => {
validateAccessCode();
}}
variant={'secondary'}
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
>
Submit
</Button>
{error && error.type === 'accesscode' && (
<div className="flex flex-col gap-3">
<div className="justify-center items-center gap-2 inline-flex">
<div className="text-red-500 text-sm">
Error: {error.message}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { AccessCode } from './AccessCode';
import { SignUp } from './SignUp';
type AccessSignUpProps = {
onDone: () => void;
};
export const AccessSignUp: React.FC<AccessSignUpProps> = ({ onDone }) => {
const [isValidAccessCode, setIsValidAccessCode] = useState<boolean>(
!!localStorage.getItem('accessCode'),
);
return isValidAccessCode ? (
<SignUp onDone={onDone} />
) : (
<AccessCode onCorrectAccessCode={() => setIsValidAccessCode(true)} />
);
};
@@ -47,7 +47,9 @@ export const Login = ({ onDone }: Props) => {
console.log('Handling google redirect back');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'login', wallet);
if (result.error) {
setError(result.error);
@@ -21,6 +21,7 @@ import {
turnkeySignin,
turnkeySignup,
} from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -45,7 +46,9 @@ export const SignUp = ({ onDone }: Props) => {
setProvider('google');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'signup', wallet);
if (result.error) {
setError({ type: 'provider', message: result.error });
@@ -109,6 +112,29 @@ export const SignUp = ({ onDone }: Props) => {
const loading = provider;
const emailValid = /.@./.test(email);
useEffect(() => {
const validateAccessCode = async () => {
const accessCode = localStorage.getItem('accessCode');
if (!accessCode) {
redirectToSignup();
return;
}
try {
await verifyAccessCode(accessCode);
} catch (err: any) {
redirectToSignup();
}
};
const redirectToSignup = () => {
localStorage.removeItem('accessCode');
window.location.href = '/signup';
};
validateAccessCode();
}, []);
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
@@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react';
import { snowball } from 'utils/use-snowball';
import { Login } from './Login';
import { SignUp } from './SignUp';
import { useEffect, useState } from 'react';
import { Done } from './Done';
import { AccessSignUp } from './AccessSignUp';
type Screen = 'login' | 'signup' | 'success';
const DASHBOARD_URL = '/';
export const SnowballAuth = () => {
export const SnowballAuth: React.FC = () => {
const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup',
@@ -30,7 +31,7 @@ export const SnowballAuth = () => {
if (screen === 'signup') {
return (
<SignUp
<AccessSignUp
onDone={() => {
setScreen('success');
}}
+28
View File
@@ -0,0 +1,28 @@
import { baseUrl } from './constants';
export async function verifyAccessCode(
accesscode: string,
): Promise<boolean | null> {
const res = await fetch(`${baseUrl}/auth/accesscode`, {
method: 'POST',
body: JSON.stringify({
accesscode,
}),
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
// If API returns a non-empty 200, this is a valid access code.
if (res.status == 200) {
const isValid = (await res.json()).isValid as boolean;
console.log('isValid', isValid);
return isValid;
} else if (res.status === 204) {
return null;
} else {
throw new Error(
`Unexpected response from access code endpoint: ${res.status}: ${await res.text()}`,
);
}
}
+11
View File
@@ -0,0 +1,11 @@
export const baseUrl = import.meta.env.VITE_SERVER_URL;
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
+8 -8
View File
@@ -1,21 +1,21 @@
import React from 'react';
import Bugsnag from '@bugsnag/js';
import BugsnagPluginReact from '@bugsnag/plugin-react';
import BugsnagPerformance from '@bugsnag/browser-performance';
import React from 'react';
const bugsnagApiKey = import.meta.env.VITE_BUGSNAG_API_KEY;
import { VITE_BUGSNAG_API_KEY } from './constants';
if (bugsnagApiKey) {
if (VITE_BUGSNAG_API_KEY) {
Bugsnag.start({
apiKey: bugsnagApiKey,
apiKey: VITE_BUGSNAG_API_KEY,
plugins: [new BugsnagPluginReact()],
});
BugsnagPerformance.start({ apiKey: bugsnagApiKey });
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
}
export const errorLoggingEnabled = !!bugsnagApiKey;
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
export const LogErrorBoundary = bugsnagApiKey
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
: ({ children }: any) => children;
@@ -28,7 +28,7 @@ export function logError(error: Error) {
}
console.error(...errors);
if (import.meta.env.VITE_BUGSNAG_API_KEY) {
if (VITE_BUGSNAG_API_KEY) {
Bugsnag.notify(error);
}
}
+3 -1
View File
@@ -2,6 +2,8 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants';
const domain = window.location.host;
const origin = window.location.origin;
@@ -17,7 +19,7 @@ export async function signInWithEthereum(
);
const signature = await wallet.signMessage(message);
const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/validate`, {
const res = await fetch(`${baseUrl}/auth/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1,10 +1,7 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
const baseUrl = import.meta.env.VITE_SERVER_URL;
const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here
+4 -3
View File
@@ -5,16 +5,17 @@ import {
LitGoogleAuth,
LitPasskeyAuth,
} from '@snowballtools/auth-lit';
import { VITE_LIT_RELAY_API_KEY } from './constants';
export const snowball = Snowball.withAuth({
google: LitGoogleAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}),
// apple: LitAppleAuth.configure({
// litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
// }),
passkey: LitPasskeyAuth.configure({
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
}),
}).create({
initialChain: SnowballChain.sepolia,
+5
View File
@@ -1,5 +1,7 @@
import { defineConfig, PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
import { exec } from 'child_process';
import { promisify } from 'util';
// https://vitejs.dev/config/
export default defineConfig({
@@ -16,5 +18,8 @@ export default defineConfig({
},
define: {
'process.env': {},
__VERSION__: JSON.stringify(
(await promisify(exec)('git rev-parse --short HEAD')).stdout.trim(),
),
},
});
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -20,7 +20,10 @@ var __async = (__this, __arguments, generator) => {
};
// src/client.ts
import { ApolloClient, InMemoryCache } from "@apollo/client";
import {
ApolloClient,
InMemoryCache
} from "@apollo/client";
// src/queries.ts
import { gql } from "@apollo/client";
File diff suppressed because one or more lines are too long
+139 -82
View File
@@ -1,8 +1,13 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import {
ApolloClient,
DefaultOptions,
InMemoryCache,
NormalizedCacheObject,
} from "@apollo/client";
import * as queries from './queries';
import * as types from './types';
import * as mutations from './mutations';
import * as queries from "./queries";
import * as types from "./types";
import * as mutations from "./mutations";
export interface GraphQLConfig {
gqlEndpoint: string;
@@ -11,13 +16,13 @@ export interface GraphQLConfig {
// TODO: check options
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore'
fetchPolicy: "no-cache",
errorPolicy: "ignore",
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all'
}
fetchPolicy: "no-cache",
errorPolicy: "all",
},
};
export class GQLClient {
@@ -28,13 +33,13 @@ export class GQLClient {
uri: config.gqlEndpoint,
cache: new InMemoryCache(),
defaultOptions,
credentials: 'include'
credentials: "include",
});
}
async getUser(): Promise<types.GetUserResponse> {
const { data } = await this.client.query({
query: queries.getUser
query: queries.getUser,
});
return data;
@@ -44,19 +49,21 @@ export class GQLClient {
const { data } = await this.client.query({
query: queries.getProject,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getProjectsInOrganization (organizationSlug: string) : Promise<types.GetProjectsInOrganizationResponse> {
async getProjectsInOrganization(
organizationSlug: string
): Promise<types.GetProjectsInOrganizationResponse> {
const { data } = await this.client.query({
query: queries.getProjectsInOrganization,
variables: {
organizationSlug
}
organizationSlug,
},
});
return data;
@@ -64,179 +71,216 @@ export class GQLClient {
async getOrganizations(): Promise<types.GetOrganizationsResponse> {
const { data } = await this.client.query({
query: queries.getOrganizations
query: queries.getOrganizations,
});
return data;
}
async getDeployments (projectId: string) : Promise<types.GetDeploymentsResponse> {
async getDeployments(
projectId: string
): Promise<types.GetDeploymentsResponse> {
const { data } = await this.client.query({
query: queries.getDeployments,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getEnvironmentVariables (projectId: string) : Promise<types.GetEnvironmentVariablesResponse> {
async getEnvironmentVariables(
projectId: string
): Promise<types.GetEnvironmentVariablesResponse> {
const { data } = await this.client.query({
query: queries.getEnvironmentVariables,
variables: {
projectId
}
projectId,
},
});
return data;
}
async getProjectMembers (projectId: string) : Promise<types.GetProjectMembersResponse> {
async getProjectMembers(
projectId: string
): Promise<types.GetProjectMembersResponse> {
const result = await this.client.query({
query: queries.getProjectMembers,
variables: {
projectId
}
projectId,
},
});
return result.data;
}
async addProjectMember (projectId: string, data: types.AddProjectMemberInput) : Promise<types.AddProjectMemberResponse> {
async addProjectMember(
projectId: string,
data: types.AddProjectMemberInput
): Promise<types.AddProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.addProjectMember,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateProjectMember (projectMemberId: string, data: types.UpdateProjectMemberInput): Promise<types.UpdateProjectMemberResponse> {
async updateProjectMember(
projectMemberId: string,
data: types.UpdateProjectMemberInput
): Promise<types.UpdateProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.updateProjectMember,
variables: {
projectMemberId,
data
}
data,
},
});
return result.data;
}
async removeProjectMember (projectMemberId: string): Promise<types.RemoveProjectMemberResponse> {
async removeProjectMember(
projectMemberId: string
): Promise<types.RemoveProjectMemberResponse> {
const result = await this.client.mutate({
mutation: mutations.removeProjectMember,
variables: {
projectMemberId
}
projectMemberId,
},
});
return result.data;
}
async searchProjects (searchText: string) : Promise<types.SearchProjectsResponse> {
async searchProjects(
searchText: string
): Promise<types.SearchProjectsResponse> {
const { data } = await this.client.query({
query: queries.searchProjects,
variables: {
searchText
}
searchText,
},
});
return data;
}
async addEnvironmentVariables (projectId: string, data: types.AddEnvironmentVariableInput[]): Promise<types.AddEnvironmentVariablesResponse> {
async addEnvironmentVariables(
projectId: string,
data: types.AddEnvironmentVariableInput[]
): Promise<types.AddEnvironmentVariablesResponse> {
const result = await this.client.mutate({
mutation: mutations.addEnvironmentVariables,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateEnvironmentVariable (environmentVariableId: string, data: types.UpdateEnvironmentVariableInput): Promise<types.UpdateEnvironmentVariableResponse> {
async updateEnvironmentVariable(
environmentVariableId: string,
data: types.UpdateEnvironmentVariableInput
): Promise<types.UpdateEnvironmentVariableResponse> {
const result = await this.client.mutate({
mutation: mutations.updateEnvironmentVariable,
variables: {
environmentVariableId,
data
}
data,
},
});
return result.data;
}
async removeEnvironmentVariable (environmentVariableId: string): Promise<types.RemoveEnvironmentVariableResponse> {
async removeEnvironmentVariable(
environmentVariableId: string
): Promise<types.RemoveEnvironmentVariableResponse> {
const { data } = await this.client.mutate({
mutation: mutations.removeEnvironmentVariable,
variables: {
environmentVariableId
}
environmentVariableId,
},
});
return data;
}
async updateDeploymentToProd (deploymentId: string): Promise<types.UpdateDeploymentToProdResponse> {
async updateDeploymentToProd(
deploymentId: string
): Promise<types.UpdateDeploymentToProdResponse> {
const { data } = await this.client.mutate({
mutation: mutations.updateDeploymentToProd,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
}
async addProject (organizationSlug: string, data: types.AddProjectInput): Promise<types.AddProjectResponse> {
async addProject(
organizationSlug: string,
data: types.AddProjectInput
): Promise<types.AddProjectResponse> {
const result = await this.client.mutate({
mutation: mutations.addProject,
variables: {
organizationSlug,
data
}
data,
},
});
return result.data;
}
async updateProject (projectId: string, data: types.UpdateProjectInput): Promise<types.UpdateProjectResponse> {
async updateProject(
projectId: string,
data: types.UpdateProjectInput
): Promise<types.UpdateProjectResponse> {
const result = await this.client.mutate({
mutation: mutations.updateProjectMutation,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async updateDomain (domainId: string, data: types.UpdateDomainInput): Promise<types.UpdateDomainResponse> {
async updateDomain(
domainId: string,
data: types.UpdateDomainInput
): Promise<types.UpdateDomainResponse> {
const result = await this.client.mutate({
mutation: mutations.updateDomainMutation,
variables: {
domainId,
data
}
data,
},
});
return result.data;
}
async redeployToProd (deploymentId: string): Promise<types.RedeployToProdResponse> {
async redeployToProd(
deploymentId: string
): Promise<types.RedeployToProdResponse> {
const { data } = await this.client.mutate({
mutation: mutations.redeployToProd,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
@@ -246,8 +290,8 @@ export class GQLClient {
const { data } = await this.client.mutate({
mutation: mutations.deleteProject,
variables: {
projectId
}
projectId,
},
});
return data;
@@ -257,66 +301,79 @@ export class GQLClient {
const { data } = await this.client.mutate({
mutation: mutations.deleteDomain,
variables: {
domainId
}
domainId,
},
});
return data;
}
async rollbackDeployment (projectId: string, deploymentId: string): Promise<types.RollbackDeploymentResponse> {
async rollbackDeployment(
projectId: string,
deploymentId: string
): Promise<types.RollbackDeploymentResponse> {
const { data } = await this.client.mutate({
mutation: mutations.rollbackDeployment,
variables: {
projectId,
deploymentId
}
deploymentId,
},
});
return data;
}
async deleteDeployment (deploymentId: string): Promise<types.DeleteDeploymentResponse> {
async deleteDeployment(
deploymentId: string
): Promise<types.DeleteDeploymentResponse> {
const { data } = await this.client.mutate({
mutation: mutations.deleteDeployment,
variables: {
deploymentId
}
deploymentId,
},
});
return data;
}
async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
async addDomain(
projectId: string,
data: types.AddDomainInput
): Promise<types.AddDomainResponse> {
const result = await this.client.mutate({
mutation: mutations.addDomain,
variables: {
projectId,
data
}
data,
},
});
return result.data;
}
async getDomains (projectId: string, filter?: types.FilterDomainInput): Promise<types.GetDomainsResponse> {
async getDomains(
projectId: string,
filter?: types.FilterDomainInput
): Promise<types.GetDomainsResponse> {
const { data } = await this.client.query({
query: queries.getDomains,
variables: {
projectId,
filter
}
filter,
},
});
return data;
}
async authenticateGitHub (code: string): Promise<types.AuthenticateGitHubResponse> {
async authenticateGitHub(
code: string
): Promise<types.AuthenticateGitHubResponse> {
const { data } = await this.client.mutate({
mutation: mutations.authenticateGitHub,
variables: {
code
}
code,
},
});
return data;
@@ -324,7 +381,7 @@ export class GQLClient {
async unauthenticateGithub(): Promise<types.UnauthenticateGitHubResponse> {
const { data } = await this.client.mutate({
mutation: mutations.unauthenticateGitHub
mutation: mutations.unauthenticateGitHub,
});
return data;
+5
View File
@@ -10575,6 +10575,11 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
express-async-errors@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
express-session@^1.18.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3"