Compare commits

..
122 changed files with 1509 additions and 3451 deletions
+1 -4
View File
@@ -6,7 +6,4 @@ yarn-error.log
packages/backend/environments/local.toml
packages/backend/dev/
packages/frontend/dist/
# ignore all .DS_Store files
**/.DS_Store
packages/frontend/dist/
+221 -13
View File
@@ -1,24 +1,232 @@
# snowballtools-base
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
## Setup
## Getting Started
- Clone the `snowballtools-base` repo
### Install dependencies
```bash
git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
```
In the root of the project, run:
- Install dependencies in root
```zsh
yarn
```
```bash
yarn
```
### Build backend
- Build packages
```zsh
yarn build --ignore frontend
```
```bash
yarn build --ignore frontend
```
### Environment variables, running the development server, and deployment
Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
- Configuration Files
- For Backend:
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `https://dashboard.snowballtools.com`
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://api.snowballtools.com"
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
```
- Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `http://localhost:3000`
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
```bash
# For registryConfig.restEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
# 0.0.0.0:32777
# For registryConfig.gqlEndpoint
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
# 0.0.0.0:32771
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
```
- Production
- Set the following values in [.env](packages/frontend/.env) file
```env
REACT_APP_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
- Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
REACT_APP_SERVER_URL = 'http://localhost:8000'
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Project home page URL is not required to be set
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
```
- The React application will be running in `http://localhost:3000/`
## Development
- Start the backend server
```bash
yarn start:backend
```
- Start the frontend
```bash
yarn start
```
+1 -76
View File
@@ -1,76 +1 @@
# backend
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
## Getting Started
### Install dependencies
In the root of the project, run:
```zsh
yarn
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
```zsh
cp envionments/local.toml.example envionments/local.toml
```
#### Staging environment variables
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
#### Production environment variables
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
### Run development server
```zsh
yarn start
```
## Deployment
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
```zsh
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
```
### Staging
```zsh
echo trigger >> .gitea/workflows/triggers/staging-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Production
```zsh
echo trigger >> .gitea/workflows/triggers/production-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Deployment status
Dumb for now
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
# Backend for Snowball Tools
@@ -3,7 +3,7 @@
port = 8000
gqlPath = "/graphql"
[server.session]
secret = ""
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
appOriginUrl = "http://localhost:3000"
trustProxy = false
domain = "localhost"
@@ -17,16 +17,6 @@
clientId = ""
clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig]
fetchDeploymentRecordDelay = 5000
restEndpoint = "http://localhost:1317"
-3
View File
@@ -1,6 +1,5 @@
{
"name": "backend",
"license": "UNLICENSED",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
@@ -19,7 +18,6 @@
"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",
@@ -45,7 +43,6 @@
"lint": "tsc --noEmit",
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
},
-13
View File
@@ -436,19 +436,6 @@ export class Database {
return Boolean(updateResult.affected);
}
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOneOrFail({
where: {
id: deploymentId
}
});
const deleteResult = await deploymentRepository.softRemove(deployment);
return Boolean(deleteResult);
}
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project);
+2 -31
View File
@@ -6,14 +6,13 @@ import {
UpdateDateColumn,
ManyToOne,
OneToOne,
JoinColumn,
DeleteDateColumn
JoinColumn
} from 'typeorm';
import { Project } from './Project';
import { Domain } from './Domain';
import { User } from './User';
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
import { AppDeploymentRecordAttributes } from '../types';
export enum Environment {
Production = 'Production',
@@ -25,7 +24,6 @@ export enum DeploymentStatus {
Building = 'Building',
Ready = 'Ready',
Error = 'Error',
Deleting = 'Deleting',
}
export interface ApplicationDeploymentRequest {
@@ -37,18 +35,6 @@ export interface ApplicationDeploymentRequest {
meta: string;
}
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: string;
}
export interface ApplicationDeploymentRemovalRequest {
type: string;
version: string;
deployment: string;
}
export interface ApplicationRecord {
type: string;
version: string;
@@ -112,18 +98,6 @@ export class Deployment {
@Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRecordId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@Column({
enum: Environment
@@ -147,7 +121,4 @@ export class Deployment {
@UpdateDateColumn()
updatedAt!: Date;
@DeleteDateColumn()
deletedAt!: Date | null;
}
-1
View File
@@ -1,4 +1,3 @@
import 'express-async-errors';
import 'reflect-metadata';
import debug from 'debug';
import fs from 'fs';
+2 -73
View File
@@ -9,19 +9,16 @@ import { RegistryConfig } from './config';
import {
ApplicationRecord,
Deployment,
ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
import { AppDeploymentRecord, PackageJSON } from './types';
import { sleep } from './utils';
const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const SLEEP_DURATION = 1000;
// TODO: Move registry code to laconic-sdk/watcher-ts
@@ -232,74 +229,6 @@ export class Registry {
);
}
/**
* Fetch ApplicationDeploymentRecords by filter
*/
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
return this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE,
...filter
},
true
);
}
/**
* Fetch ApplicationDeploymentRemovalRecords for deployments
*/
async getDeploymentRemovalRecords (
deployments: Deployment[]
): Promise<AppDeploymentRemovalRecord[]> {
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
},
true
);
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs
return records.filter((record: AppDeploymentRemovalRecord) =>
deployments.some(
(deployment) =>
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
deployment.applicationDeploymentRecordId === record.attributes.deployment
)
);
}
async createApplicationDeploymentRemovalRequest (data: {
deploymentId: string;
}): Promise<{
applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
}> {
const applicationDeploymentRemovalRequest = {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0',
deployment: data.deploymentId
};
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRemovalRequest,
bondId: this.registryConfig.bondId
},
'',
this.registryConfig.fee
);
log(`Application deployment removal request record published: ${result.data.id}`);
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
return {
applicationDeploymentRemovalRequestId: result.data.id,
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
};
}
getCrn (appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist");
return `crn://${this.registryConfig.authority}/applications/${appName}`;
+36 -89
View File
@@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain';
import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { AddProjectFromTemplateInput } from './types';
const log = debug('snowball:resolver');
@@ -18,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return context.user;
},
organizations: async (_: any, __: any, context: any) => {
organizations: async (_:any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.user);
},
@@ -26,15 +25,8 @@ 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 }) => {
@@ -43,7 +35,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string },
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -52,11 +44,7 @@ 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);
},
@@ -64,11 +52,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
@@ -76,13 +64,10 @@ 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;
@@ -93,13 +78,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);
@@ -113,14 +98,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));
@@ -134,15 +119,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);
@@ -154,16 +139,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);
@@ -173,7 +158,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string },
{ environmentVariableId }: { environmentVariableId: string }
) => {
try {
return await service.removeEnvironmentVariable(environmentVariableId);
@@ -186,45 +171,23 @@ 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;
}
},
addProjectFromTemplate: async (
_: any,
{
organizationSlug,
data,
}: { organizationSlug: string; data: AddProjectFromTemplateInput },
context: any,
) => {
try {
return await service.addProjectFromTemplate(
context.user,
organizationSlug,
data,
);
} catch (err) {
log(err);
throw err;
}
},
addProject: async (
_: any,
{
organizationSlug,
data,
data
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any,
context: any
) => {
try {
return await service.addProject(context.user, organizationSlug, data);
@@ -236,7 +199,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);
@@ -249,12 +212,10 @@ 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;
@@ -283,8 +244,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);
@@ -294,21 +255,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
}
},
deleteDeployment: async (
_: any,
{ deploymentId }: { deploymentId: string },
) => {
try {
return await service.deleteDeployment(deploymentId);
} catch (err) {
log(err);
return false;
}
},
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));
@@ -320,7 +269,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);
@@ -333,7 +282,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);
@@ -345,14 +294,12 @@ 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;
}
},
},
}
}
};
};
-15
View File
@@ -5,19 +5,6 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
//
// Access Code
//
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '44444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
//
// Turnkey
//
@@ -32,7 +19,6 @@ 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,
@@ -45,7 +31,6 @@ 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
@@ -1,9 +0,0 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.9' });
});
export default router;
-14
View File
@@ -19,7 +19,6 @@ enum DeploymentStatus {
Building
Ready
Error
Deleting
}
enum DomainStatus {
@@ -130,14 +129,6 @@ input AddEnvironmentVariableInput {
value: String!
}
input AddProjectFromTemplateInput {
templateOwner: String!
templateRepo: String!
owner: String!
name: String!
isPrivate: Boolean!
}
input AddProjectInput {
name: String!
repository: String!
@@ -212,17 +203,12 @@ type Mutation {
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean!
addProjectFromTemplate(
organizationSlug: String!
data: AddProjectFromTemplateInput
): Project!
addProject(organizationSlug: String!, data: AddProjectInput): Project!
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean!
deleteDomain(domainId: String!): Boolean!
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
deleteDeployment(deploymentId: String!): Boolean!
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
authenticateGitHub(code: String!): AuthResult!
-7
View File
@@ -17,7 +17,6 @@ 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');
@@ -110,12 +109,6 @@ 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}`);
File diff suppressed because it is too large Load Diff
-19
View File
@@ -38,13 +38,6 @@ export interface AppDeploymentRecordAttributes {
version: string;
}
export interface AppDeploymentRemovalRecordAttributes {
deployment: string;
request: string;
type: 'ApplicationDeploymentRemovalRecord';
version: string;
}
interface RegistryRecord {
id: string;
names: string[] | null;
@@ -57,15 +50,3 @@ interface RegistryRecord {
export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes;
}
export interface AppDeploymentRemovalRecord extends RegistryRecord {
attributes: AppDeploymentRemovalRecordAttributes;
}
export interface AddProjectFromTemplateInput {
templateOwner: string;
templateRepo: string;
owner: string;
name: string;
isPrivate: boolean;
}
@@ -7,7 +7,7 @@ import { Registry } from '@snowballtools/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records');
@@ -40,7 +40,7 @@ async function main () {
});
for await (const deployment of deployments) {
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
const applicationDeploymentRecord = {
type: 'ApplicationDeploymentRecord',
@@ -71,21 +71,6 @@ async function main () {
registryConfig.fee
);
// Remove deployment for project subdomain if deployment is for production environment
if (deployment.environment === Environment.Production) {
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
}
log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.data.id}`);
}
@@ -1,67 +0,0 @@
import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { Registry } from '@cerc-io/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
const log = debug('snowball:publish-deployment-removal-records');
async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
registryConfig.chainId
);
const dataSource = new DataSource({
type: 'better-sqlite3',
database: database.dbPath,
synchronize: true,
entities: [path.join(__dirname, '../src/entity/*')]
});
await dataSource.initialize();
const deploymentRepository = dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find({
relations: {
project: true
},
where: {
status: DeploymentStatus.Deleting
}
});
for await (const deployment of deployments) {
const applicationDeploymentRemovalRecord = {
type: "ApplicationDeploymentRemovalRecord",
version: "1.0.0",
deployment: deployment.applicationDeploymentRecordId,
request: deployment.applicationDeploymentRemovalRequestId,
}
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
record: applicationDeploymentRemovalRecord,
bondId: registryConfig.bondId
},
'',
registryConfig.fee
);
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
log(`Application deployment removal record published: ${result.data.id}`);
}
}
main().catch((err) => {
log(err);
});
+2 -9
View File
@@ -1,11 +1,9 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
@@ -14,13 +12,13 @@
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
```bash
```
./deploy-frontend.staging.sh
```
- To deploy frontend app to `dashboard.apps.snowballtools.com`
```bash
```
./deploy-frontend.sh
```
@@ -29,7 +27,6 @@
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
@@ -38,7 +35,6 @@
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
...
```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot
@@ -48,7 +44,6 @@
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic cns account get
@@ -56,9 +51,7 @@
# Bond balance
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
```
- Command to refill bond
```bash
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
```
+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: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
chainId: laconic_9000-1
gas: 1200000
gas: 550000
fees: 200000aphoton
+8 -40
View File
@@ -26,7 +26,7 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
@@ -37,9 +37,9 @@ record:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -48,7 +48,7 @@ record:
EOF
# Generate application-record.yml with incremented version
cat >./records/application-record.yml <<EOF
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -64,13 +64,7 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
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')
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
@@ -79,36 +73,15 @@ 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
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]')
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -117,11 +90,6 @@ fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
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')
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+10 -42
View File
@@ -26,7 +26,7 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi
# Generate application-deployment-request.yml
cat >./staging-records/application-deployment-request.yml <<EOF
cat > ./staging-records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
@@ -36,16 +36,16 @@ record:
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
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: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
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
@@ -54,7 +54,7 @@ record:
EOF
# Generate application-record.yml with incremented version
cat >./staging-records/application-record.yml <<EOF
cat > ./staging-records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -70,13 +70,7 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
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')
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
@@ -85,36 +79,15 @@ 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
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]')
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_CRN."
exit 1
@@ -123,11 +96,6 @@ fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
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')
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
@@ -1,6 +1,6 @@
record:
type: ApplicationDeploymentRequest
version: "1.0.0"
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
@@ -8,9 +8,9 @@ record:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
@@ -7,18 +7,18 @@ record:
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
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: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
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 @ Mon Jun 24 23:51:48 UTC 2024
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
@@ -1,7 +1,7 @@
record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
version: 0.0.2
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: staging-snowballtools-base-frontend
+1 -1
View File
@@ -2,7 +2,7 @@
Check if the live web app deployer is in a working state
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
- Config used: [../config.yml](../config.yml)
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
@@ -1,6 +1,6 @@
record:
type: ApplicationDeploymentRequest
version: "1.0.0"
version: '1.0.0'
name: deployment-test-app@0.1.24
application: crn://snowballtools/applications/deployment-test-app@0.1.24
dns: deployment-ci-test
@@ -11,5 +11,5 @@ record:
CERC_WEBAPP_DEBUG: 0
meta:
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
repository: "https://github.com/snowball-tools/test-progressive-web-app"
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
@@ -2,7 +2,7 @@ record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
app_type: webapp
name: deployment-test-app
app_version: 0.1.24
@@ -1,7 +1,7 @@
#!/bin/bash
# Repository URL
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
@@ -32,7 +32,7 @@ fi
# Generate application-record.yml with incremented version
RECORD_FILE=packages/deployer/test/records/application-record.yml
cat >$RECORD_FILE <<EOF
cat > $RECORD_FILE <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -46,7 +46,7 @@ EOF
# Generate application-deployment-request.yml
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
cat >$REQUEST_RECORD_FILE <<EOF
cat > $REQUEST_RECORD_FILE <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
@@ -115,7 +115,7 @@ while true; do
else
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRecord found"
@@ -154,7 +154,7 @@ while true; do
else
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
fi
done
@@ -162,7 +162,7 @@ done
# Generate application-deployment-removal-request.yml
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
record:
deployment: $DEPLOYMENT_RECORD_ID
type: ApplicationDeploymentRemovalRequest
@@ -190,7 +190,7 @@ while true; do
else
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRemovalRecord found"
@@ -217,7 +217,7 @@ while true; do
else
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
fi
done
+1 -1
View File
@@ -14,4 +14,4 @@ VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID=
VITE_TURNKEY_ORGANIZATION_ID=
-1
View File
@@ -1 +0,0 @@
dist/
-2
View File
@@ -9,7 +9,6 @@ import { join, dirname } from 'path';
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
@@ -27,7 +26,6 @@ const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
};
export default config;
+23 -58
View File
@@ -1,65 +1,30 @@
# frontend
# React + TypeScript + Vite
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Getting Started
Currently, two official plugins are available:
### Install dependencies
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
In the root of the project, run:
## Expanding the ESLint configuration
```zsh
yarn
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
};
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `.env.example` file to `.env`:
```zsh
cp .env.example .env
```
#### Staging environment variables
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
#### Production environment variables
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
### Run development server
```zsh
yarn dev
```
## Deployment
From the root of the project,
- check if the [gittea repository mirror](https://git.vdb.to/cerc-io/snowballtools-base) is up to date
### Staging
```zsh
cd packages/deployer && ./deploy-frontend.staging.sh
```
### Production
```zsh
cd packages/deployer && ./deploy-frontend.sh
```
### Deployment status
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
+2 -1
View File
@@ -62,6 +62,7 @@
"react-dom": "^18.2.0",
"react-dropdown": "^1.11.0",
"react-hook-form": "^7.49.0",
"react-hot-toast": "^2.4.1",
"react-oauth-popup": "^1.0.5",
"react-router-dom": "^6.20.1",
"react-timer-hook": "^3.0.7",
@@ -101,4 +102,4 @@
"typescript": "^5.3.3",
"vite": "^5.2.0"
}
}
}
+2 -3
View File
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug';
@@ -11,8 +10,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([
{
@@ -60,7 +59,7 @@ function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${baseUrl}/auth/session`, {
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
if (res.status !== 200) {
+2 -7
View File
@@ -1,21 +1,16 @@
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: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
repoFullName: `${import.meta.env.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 }, ref) => {
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
return (
<div className="relative flex w-full">
<Input
@@ -18,7 +18,6 @@ const SearchBar: React.ForwardRefRenderFunction<
appearance="borderless"
className="w-full lg:w-[459px]"
{...props}
ref={ref}
/>
</div>
);
@@ -53,16 +53,6 @@ export const ProjectCard = ({
navigate(`projects/${project.id}`);
}, [project.id, navigate]);
const navigateToSettingsOnClick = useCallback(
(
e: React.MouseEvent<HTMLLIElement> | React.MouseEvent<HTMLButtonElement>,
) => {
e.stopPropagation();
navigate(`projects/${project.id}/settings`);
},
[project.id, navigate],
);
return (
<div
{...props}
@@ -102,15 +92,8 @@ export const ProjectCard = ({
</Button>
</MenuHandler>
<MenuList>
<MenuItem onClick={navigateToSettingsOnClick}>
Project settings
</MenuItem>
<MenuItem
className="text-red-500"
onClick={navigateToSettingsOnClick}
>
Delete project
</MenuItem>
<MenuItem>Project settings</MenuItem>
<MenuItem className="text-red-500">Delete project</MenuItem>
</MenuList>
</Menu>
</div>
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { useDebounceValue } from 'usehooks-ts';
import { useDebounce } from 'usehooks-ts';
import SearchBar from 'components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext';
@@ -42,7 +42,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
@@ -62,7 +62,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
<div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} />
<div
{...getMenuProps({}, { suppressRefError: true })}
{...getMenuProps()}
className={cn(
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen },
@@ -5,7 +5,7 @@ import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useDebounceValue } from 'usehooks-ts';
import { useDebounce } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { useNavigate } from 'react-router-dom';
@@ -27,30 +27,25 @@ export const ProjectSearchBarDialog = ({
const client = useGQLClient();
const navigate = useNavigate();
const {
getInputProps,
getItemProps,
getMenuProps,
inputValue,
setInputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const { getInputProps, getItemProps, inputValue, setInputValue } =
useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
@@ -80,7 +75,7 @@ export const ProjectSearchBarDialog = ({
<div className="h-full flex flex-col fixed top-0 inset-0">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<Input
{...getInputProps({}, { suppressRefError: true })}
{...getInputProps()}
leftIcon={<SearchIcon />}
placeholder="Search"
appearance="borderless"
@@ -91,33 +86,23 @@ export const ProjectSearchBarDialog = ({
</Button>
</div>
{/* Content */}
<div
className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps(
{},
{
suppressRefError: true,
},
)}
>
{items.length > 0 ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
))}
</>
) : (
inputValue && <ProjectSearchBarEmpty />
)}
<div className="flex flex-col gap-1 px-2 py-2">
{items.length > 0
? items.map((item, index) => (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
<ProjectSearchBarItem
{...getItemProps({ item, index })}
key={item.id}
item={item}
/>
</>
))
: inputValue && <ProjectSearchBarEmpty />}
</div>
</div>
</Dialog.Content>
@@ -14,10 +14,11 @@ 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=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface {
onAuth: (token: string) => void;
@@ -155,13 +155,13 @@ export const RepositoryList = () => {
{Boolean(repositoryDetails.length) ? (
<div className="flex flex-col gap-2">
{repositoryDetails.map((repo, index) => (
<div key={index}>
<ProjectRepoCard repository={repo} />
<>
<ProjectRepoCard repository={repo} key={index} />
{/* Horizontal line */}
{index !== repositoryDetails.length - 1 && (
<div className="border-b border-border-separator/[0.06] w-full" />
)}
</div>
</>
))}
</div>
) : (
@@ -1,8 +1,13 @@
import { CopyBlock, atomOneLight } from 'react-code-blocks';
import { Link } from 'react-router-dom';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
} from '@snowballtools/material-tailwind-react-fork';
interface AssignDomainProps {
open: boolean;
@@ -11,31 +16,36 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<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{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
showLineNumbers={false}
theme={atomOneLight}
/>
</Modal.Body>
<Modal.Footer className="flex justify-start">
<Button onClick={handleOpen}>Okay</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
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{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
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>
);
};
@@ -38,7 +38,6 @@ const STATUS_COLORS: {
[DeploymentStatus.Building]: 'emphasized',
[DeploymentStatus.Ready]: 'positive',
[DeploymentStatus.Error]: 'negative',
[DeploymentStatus.Deleting]: 'neutral',
};
const DeploymentDetailsCard = ({
@@ -49,10 +48,7 @@ const DeploymentDetailsCard = ({
prodBranchDomains,
}: DeployDetailsCardProps) => {
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
if (
status === DeploymentStatus.Building ||
status === DeploymentStatus.Deleting
) {
if (status === DeploymentStatus.Building) {
return <LoadingIcon className="animate-spin" />;
}
if (status === DeploymentStatus.Ready) {
@@ -1,12 +1,5 @@
import { useState, ComponentPropsWithRef } from 'react';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { Deployment, Domain, Environment, Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import {
@@ -16,13 +9,18 @@ import {
RefreshIcon,
RocketIcon,
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;
@@ -42,7 +40,6 @@ export const DeploymentMenu = ({
...props
}: DeploymentMenuProps) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false);
@@ -53,19 +50,9 @@ export const DeploymentMenu = ({
const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) {
await onUpdate();
toast({
id: 'deployment_changed_to_production',
title: 'Deployment changed to production',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Deployment changed to production');
} else {
toast({
id: 'deployment_not_changed_to_production',
title: 'Error changing deployment to production',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to change deployment to production');
}
};
@@ -73,19 +60,9 @@ export const DeploymentMenu = ({
const isRedeployed = await client.redeployToProd(deployment.id);
if (isRedeployed) {
await onUpdate();
toast({
id: 'redeployed_to_production',
title: 'Redeployed to production',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Redeployed to production');
} else {
toast({
id: 'redeployed_to_production_failed',
title: 'Error redeploying to production',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to redeploy to production');
}
};
@@ -96,39 +73,9 @@ export const DeploymentMenu = ({
);
if (isRollbacked) {
await onUpdate();
toast({
id: 'deployment_rolled_back',
title: 'Deployment rolled back',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Deployment rolled back');
} else {
toast({
id: 'deployment_rollback_failed',
title: 'Error rolling back deployment',
variant: 'error',
onDismiss: dismiss,
});
}
};
const deleteDeployment = async () => {
const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) {
await onUpdate();
toast({
id: 'deployment_deleted',
title: 'Deployment deleted',
variant: 'success',
onDismiss: dismiss,
});
} else {
toast({
id: 'deployment_not_deleted',
title: 'Error deleting deployment',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to rollback deployment');
}
};
@@ -200,12 +147,6 @@ export const DeploymentMenu = ({
>
<UndoIcon /> Rollback to this version
</MenuItem>
<MenuItem
className="hover:bg-base-bg-emphasized flex items-center gap-3"
onClick={() => deleteDeployment()}
>
<CrossCircleIcon /> Delete deployment
</MenuItem>
</MenuList>
</Menu>
</div>
@@ -19,7 +19,7 @@ const AddEnvironmentVariableRow = ({
isDeleteDisabled,
}: AddEnvironmentVariableRowProps) => {
return (
<div className="flex gap-2 py-1 self-stretch items-end">
<div className="flex py-4 self-stretch">
<Input
size="md"
{...register(`variables.${index}.key`, {
@@ -34,9 +34,16 @@ const AddEnvironmentVariableRow = ({
required: 'Value field cannot be empty',
})}
/>
<Button size="md" iconOnly onClick={onDelete} disabled={isDeleteDisabled}>
<TrashIcon />
</Button>
<div className="self-end">
<Button
size="md"
iconOnly
onClick={onDelete}
disabled={isDeleteDisabled}
>
<TrashIcon />
</Button>
</div>
</div>
);
};
@@ -1,13 +1,13 @@
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { AddProjectMemberInput, Permission } from 'gql-client';
import { Typography } from '@snowballtools/material-tailwind-react-fork';
import { Button } from 'components/shared/Button';
import { Modal } from 'components/shared/Modal';
import { Input } from 'components/shared/Input';
import { Select, SelectOption } from 'components/shared/Select';
import { AddProjectMemberInput, Permission } from 'gql-client';
import { Checkbox } from 'components/shared/Checkbox';
interface AddMemberDialogProp {
open: boolean;
@@ -17,30 +17,18 @@ interface AddMemberDialogProp {
interface formData {
emailAddress: string;
canEdit: boolean;
permissions: {
view: boolean;
edit: boolean;
};
}
const permissionViewOptions: SelectOption = {
value: Permission.View,
label: Permission.View,
};
const permissionEditOptions: SelectOption = {
value: Permission.Edit,
label: Permission.Edit,
};
const permissionsDropdownOptions: SelectOption[] = [
permissionViewOptions,
permissionEditOptions,
];
const AddMemberDialog = ({
open,
handleOpen,
handleAddMember,
}: AddMemberDialogProp) => {
const {
watch,
setValue,
handleSubmit,
register,
reset,
@@ -48,7 +36,10 @@ const AddMemberDialog = ({
} = useForm({
defaultValues: {
emailAddress: '',
canEdit: false,
permissions: {
view: true,
edit: false,
},
},
});
@@ -56,7 +47,11 @@ const AddMemberDialog = ({
reset();
handleOpen();
const permissions = [data.canEdit ? Permission.Edit : Permission.View];
const permissions = Object.entries(data.permissions)
.filter(([, value]) => value)
.map(
([key]) => key.charAt(0).toUpperCase() + key.slice(1),
) as Permission[];
await handleAddMember({ email: data.emailAddress, permissions });
}, []);
@@ -77,19 +72,19 @@ const AddMemberDialog = ({
required: 'email field cannot be empty',
})}
/>
<Select
label="Permissions"
description="You can change this later if required."
options={permissionsDropdownOptions}
value={
watch('canEdit') ? permissionEditOptions : permissionViewOptions
}
onChange={(value) =>
setValue(
'canEdit',
(value as SelectOption)!.value === Permission.Edit,
)
}
<Typography variant="small">Permissions</Typography>
<Typography variant="small">
You can change this later if required.
</Typography>
<Checkbox
label={Permission.View}
{...register(`permissions.view`)}
color="blue"
/>
<Checkbox
label={Permission.Edit}
{...register(`permissions.edit`)}
color="blue"
/>
</Modal.Body>
<Modal.Footer>
@@ -1,14 +1,21 @@
import { useCallback } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { useGQLClient } from 'context/GQLClientContext';
import { useToast } from 'components/shared/Toast';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
import { Input } from 'components/shared/Input';
import { Project } from 'gql-client';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
Input,
Typography,
} from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { useToast } from 'components/shared/Toast';
interface DeleteProjectDialogProp {
open: boolean;
handleOpen: () => void;
@@ -53,36 +60,51 @@ const DeleteProjectDialog = ({
}, [client, project, handleOpen]);
return (
<Modal open={open} onOpenChange={handleOpen}>
<Modal.Content>
<Modal.Header>Delete project?</Modal.Header>
<form onSubmit={handleSubmit(deleteProjectHandler)}>
<Modal.Body>
<Input
label={
"Deleting your project is irreversible. Enter your project's name " +
project.name +
' below to confirm you want to permanently delete it:'
}
id="input"
{...register('projectName', {
required: 'Project name is required',
validate: (value) => value === project.name,
})}
helperText="Deleting your project is irreversible."
/>
</Modal.Body>
<Modal.Footer className="flex justify-start">
<Button onClick={handleOpen} variant="tertiary">
Cancel
</Button>
<Button variant="danger" type="submit" disabled={!isValid}>
Yes, delete project
</Button>
</Modal.Footer>
</form>
</Modal.Content>
</Modal>
<Dialog open={open} handler={handleOpen}>
<DialogHeader className="flex justify-between">
<div>Delete project?</div>
<Button
variant="outlined"
onClick={handleOpen}
className="mr-1 rounded-3xl"
>
X
</Button>
</DialogHeader>
<form onSubmit={handleSubmit(deleteProjectHandler)}>
<DialogBody className="flex flex-col gap-2">
<Typography variant="paragraph">
Deleting your project is irreversible. Enter your projects
name&nbsp;
<span className="bg-blue-100 text-blue-700">({project.name})</span>
&nbsp;below to confirm you want to permanently delete it:
</Typography>
<Input
id="input"
{...register('projectName', {
required: 'Project name is required',
validate: (value) => value === project.name,
})}
/>
<Typography variant="small" color="red">
^ Deleting your project is irreversible.
</Typography>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button variant="outlined" onClick={handleOpen} className="mr-1">
Cancel
</Button>
<Button
variant="gradient"
color="red"
type="submit"
disabled={!isValid}
>
Yes, Delete project
</Button>
</DialogFooter>
</form>
</Dialog>
);
};
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Domain, DomainStatus, Project } from 'gql-client';
import {
Chip,
Typography,
Menu,
MenuHandler,
@@ -14,15 +15,6 @@ import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
import { useToast } from 'components/shared/Toast';
import { Tag } from 'components/shared/Tag';
import {
CheckIcon,
CrossIcon,
GearIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
enum RefreshStatus {
IDLE,
@@ -87,29 +79,22 @@ const DomainCard = ({
<>
<div className="flex justify-between py-3">
<div className="flex justify-start gap-1">
<Heading as="h6" className="flex-col">
{domain.name}{' '}
<Tag
type={
domain.status === DomainStatus.Live ? 'positive' : 'negative'
}
leftIcon={
domain.status === DomainStatus.Live ? (
<CheckIcon />
) : (
<CrossIcon />
)
}
>
{domain.status}
</Tag>
</Heading>
<Typography variant="h6">
<i>^</i> {domain.name}
</Typography>
<Chip
className="w-fit capitalize"
value={domain.status}
color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
variant="ghost"
icon={<i>^</i>}
/>
</div>
<div className="flex justify-start gap-1">
<i
id="refresh"
className="cursor-pointer"
className="cursor-pointer w-8 h-8"
onClick={() => {
SetRefreshStatus(RefreshStatus.CHECKING);
setTimeout(() => {
@@ -117,17 +102,11 @@ const DomainCard = ({
}, CHECK_FAIL_TIMEOUT);
}}
>
{refreshStatus === RefreshStatus.CHECKING ? (
<LoadingIcon className="animate-spin" />
) : (
'L'
)}
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'}
</i>
<Menu placement="bottom-end">
<MenuHandler>
<Button iconOnly>
<GearIcon />
</Button>
<button className="border-2 rounded-full w-8 h-8">...</button>
</MenuHandler>
<MenuList>
<MenuItem
@@ -164,13 +143,13 @@ const DomainCard = ({
{domain.status === DomainStatus.Pending && (
<Card className="bg-slate-100 p-4 text-sm">
{refreshStatus === RefreshStatus.IDLE ? (
<Heading>
<Typography variant="small">
^ Add these records to your domain and refresh to check
</Heading>
</Typography>
) : refreshStatus === RefreshStatus.CHECKING ? (
<Heading className="text-blue-500">
<Typography variant="small" className="text-blue-500">
^ Checking records for {domain.name}
</Heading>
</Typography>
) : (
<div className="flex gap-2 text-red-500 mb-2">
<div className="grow">
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Domain } from 'gql-client';
import {
@@ -8,11 +9,10 @@ import {
Option,
} from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
const DEFAULT_REDIRECT_OPTIONS = ['none'];
@@ -40,7 +40,6 @@ const EditDomainDialog = ({
onUpdate,
}: EditDomainDialogProp) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const getRedirectUrl = (domain: Domain) => {
const redirectDomain = domain.redirectTo;
@@ -100,20 +99,10 @@ const EditDomainDialog = ({
if (updateDomain) {
await onUpdate();
toast({
id: 'domain_id_updated',
title: `Domain ${domain.name} has been updated`,
variant: 'success',
onDismiss: dismiss,
});
toast.success(`Domain ${domain.name} has been updated`);
} else {
reset();
toast({
id: 'domain_id_error_update',
title: `Error updating domain ${domain.name}`,
variant: 'error',
onDismiss: dismiss,
});
toast.error(`Error updating domain ${domain.name}`);
}
handleOpen();
@@ -1,20 +1,16 @@
import { useCallback, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { EnvironmentVariable } from 'gql-client';
import {
IconButton,
Typography,
} from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from 'context/GQLClientContext';
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
import { Input } from 'components/shared/Input';
import { Button } from 'components/shared/Button';
import {
CheckRoundFilledIcon,
CrossIcon,
EditBigIcon,
HideEyeOffIcon,
ShowEyeIcon,
TrashIcon,
} from 'components/shared/CustomIcon';
import { EnvironmentVariable } from 'gql-client';
import { useToast } from 'components/shared/Toast';
const ShowPasswordIcon = ({
handler,
@@ -30,24 +26,19 @@ const ShowPasswordIcon = ({
}}
className="cursor-pointer"
>
{isVisible ? <ShowEyeIcon size={16} /> : <HideEyeOffIcon size={16} />}
{isVisible ? '-' : '+'}
</span>
);
};
export interface EditEnvironmentVariableRowProps {
variable: EnvironmentVariable;
onUpdate: () => Promise<void>;
isFirstVariable?: boolean;
}
const EditEnvironmentVariableRow = ({
variable,
onUpdate,
isFirstVariable = false,
}: EditEnvironmentVariableRowProps) => {
}: {
variable: EnvironmentVariable;
onUpdate: () => Promise<void>;
}) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const { handleSubmit, register, reset } = useForm({
defaultValues: {
@@ -67,19 +58,9 @@ const EditEnvironmentVariableRow = ({
if (isEnvironmentVariableRemoved) {
await onUpdate();
setDeleteDialogOpen((preVal) => !preVal);
toast({
id: 'variable_deleted',
title: 'Variable deleted',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Variable deleted');
} else {
toast({
id: 'variable_not_deleted',
title: 'Variable not deleted',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Variable not deleted');
}
}, [variable, onUpdate]);
@@ -90,21 +71,10 @@ const EditEnvironmentVariableRow = ({
if (isEnvironmentVariableUpdated) {
await onUpdate();
toast({
id: 'variable_updated',
title: 'Variable edited',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Variable edited');
setEdit((preVal) => !preVal);
} else {
toast({
id: 'variable_not_updated',
title: 'Variable not edited',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Variable not edited');
}
},
[variable, onUpdate],
@@ -116,55 +86,71 @@ const EditEnvironmentVariableRow = ({
return (
<>
<div className="flex gap-1 items-end">
<Input
disabled={!edit}
{...register(`key`)}
label={isFirstVariable ? 'Key' : undefined}
/>
<Input
disabled={!edit}
type={showPassword || edit ? 'text' : 'password'}
label={isFirstVariable ? 'Value' : undefined}
rightIcon={
<ShowPasswordIcon
handler={() => {
setShowPassword((prevShowPassword) => !prevShowPassword);
}}
isVisible={showPassword || edit}
/>
}
{...register(`value`)}
/>
<Button
iconOnly
size="md"
onClick={() => {
edit
? handleSubmit(updateEnvironmentVariableHandler)()
: setEdit((preVal) => !preVal);
}}
>
{edit ? (
<CheckRoundFilledIcon size={16} />
) : (
<EditBigIcon size={16} />
)}
</Button>
<Button
iconOnly
size="md"
onClick={() => {
if (edit) {
reset();
setEdit((preVal) => !preVal);
} else {
setDeleteDialogOpen((preVal) => !preVal);
<div className="flex gap-1 p-2">
<div>
<Typography variant="small">Key</Typography>
<Input disabled={!edit} {...register(`key`)} />
</div>
<div>
<Typography variant="small">Value</Typography>
<Input
disabled={!edit}
type={showPassword ? 'text' : 'password'}
leftIcon={
<ShowPasswordIcon
handler={() => {
setShowPassword((prevShowPassword) => !prevShowPassword);
}}
isVisible={showPassword}
/>
}
}}
>
{edit ? <CrossIcon size={16} /> : <TrashIcon size={16} />}
</Button>
{...register(`value`)}
/>
</div>
{edit ? (
<>
<div className="self-end">
<IconButton
onClick={handleSubmit(updateEnvironmentVariableHandler)}
size="sm"
>
{'S'}
</IconButton>
</div>
<div className="self-end">
<IconButton
size="sm"
onClick={() => {
reset();
setEdit((preVal) => !preVal);
}}
>
{'C'}
</IconButton>
</div>
</>
) : (
<>
<div className="self-end">
<IconButton
size="sm"
onClick={() => {
setEdit((preVal) => !preVal);
}}
>
{'E'}
</IconButton>
</div>
<div className="self-end">
<IconButton
size="sm"
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
>
{'D'}
</IconButton>
</div>
</>
)}
</div>
<DeleteVariableDialog
handleCancel={() => setDeleteDialogOpen((preVal) => !preVal)}
@@ -80,7 +80,7 @@ const MemberCard = ({
return (
<div
className={`flex py-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
className={`flex p-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
>
<div className="basis-1/2">
{member.name && (
@@ -3,8 +3,6 @@ import { useState } from 'react';
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { Input } from 'components/shared/Input';
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
interface WebhookCardProps {
webhookUrl: string;
@@ -16,12 +14,11 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
return (
<div className="flex justify-between w-full mb-3 gap-3">
<Input value={webhookUrl} disabled />
<div className="flex justify-between w-full mb-3">
{webhookUrl}
<div className="flex gap-3">
<Button
iconOnly
size="md"
size="sm"
onClick={() => {
navigator.clipboard.writeText(webhookUrl);
toast({
@@ -32,17 +29,16 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
});
}}
>
<CopyIcon />
Copy
</Button>
<Button
iconOnly
size="md"
size="sm"
variant="danger"
onClick={() => {
setDeleteDialogOpen(true);
}}
>
<TrashIcon />
X
</Button>
</div>
<DeleteWebhookDialog
@@ -30,7 +30,7 @@ abbr[title] {
@apply !text-elements-disabled !bg-transparent;
}
.react-calendar__month-view__days__day--neighboringMonth:focus-visible {
.react-calendar__month-view__days__day--neighboringMonth {
@apply !text-elements-disabled !bg-transparent;
}
@@ -12,7 +12,7 @@ export const CollaboratorsIcon = (props: CustomIconProps) => {
<path
d="M12.5002 2.08301C14.3411 2.08301 15.8335 3.57539 15.8335 5.41634C15.8335 7.25729 14.3411 8.74968 12.5002 8.74968M14.5835 11.1659C17.2451 12.0374 19.1668 14.498 19.1668 17.083H16.6668M7.50016 8.74968C5.65921 8.74968 4.16683 7.25729 4.16683 5.41634C4.16683 3.57539 5.65921 2.08301 7.50016 2.08301C9.34111 2.08301 10.8335 3.57539 10.8335 5.41634C10.8335 7.25729 9.34111 8.74968 7.50016 8.74968ZM0.833496 17.083C0.833496 13.8613 3.81826 10.833 7.50016 10.833C11.1821 10.833 14.1668 13.8613 14.1668 17.083H0.833496Z"
stroke="currentColor"
strokeLinecap="square"
stroke-linecap="square"
/>
</CustomIcon>
);
@@ -12,8 +12,8 @@ export const CopyUnfilledIcon = (props: CustomIconProps) => {
<path
d="M6 5.625V5.025C6 4.18492 6 3.76488 6.16349 3.44401C6.3073 3.16177 6.53677 2.9323 6.81901 2.78849C7.13988 2.625 7.55992 2.625 8.4 2.625H12.975C13.8151 2.625 14.2351 2.625 14.556 2.78849C14.8382 2.9323 15.0677 3.16177 15.2115 3.44401C15.375 3.76488 15.375 4.18492 15.375 5.025V9.975C15.375 10.8151 15.375 11.2351 15.2115 11.556C15.0677 11.8382 14.8382 12.0677 14.556 12.2115C14.2351 12.375 13.8151 12.375 12.975 12.375H12.375M12.375 8.025V12.975C12.375 13.8151 12.375 14.2351 12.2115 14.556C12.0677 14.8382 11.8382 15.0677 11.556 15.2115C11.2351 15.375 10.8151 15.375 9.975 15.375H5.025C4.18492 15.375 3.76488 15.375 3.44401 15.2115C3.16177 15.0677 2.9323 14.8382 2.78849 14.556C2.625 14.2351 2.625 13.8151 2.625 12.975V8.025C2.625 7.18492 2.625 6.76488 2.78849 6.44401C2.9323 6.16177 3.16177 5.9323 3.44401 5.78849C3.76488 5.625 4.18492 5.625 5.025 5.625H9.975C10.8151 5.625 11.2351 5.625 11.556 5.78849C11.8382 5.9323 12.0677 6.16177 12.2115 6.44401C12.375 6.76488 12.375 7.18492 12.375 8.025Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
stroke-linecap="round"
stroke-linejoin="round"
/>
</CustomIcon>
);
@@ -1,26 +0,0 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const EditBigIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.61304 1.99935L7.5 1.99935C7.77614 1.99935 8 2.22321 8 2.49935C8 2.77549 7.77614 2.99935 7.5 2.99935H4.63333C4.25171 2.99935 3.99557 2.99974 3.79832 3.01585C3.6069 3.03149 3.51538 3.05941 3.45501 3.09018C3.29821 3.17007 3.17072 3.29755 3.09083 3.45436C3.06007 3.51473 3.03214 3.60625 3.01651 3.79767C3.00039 3.99492 3 4.25106 3 4.63268V11.366C3 11.7476 3.00039 12.0038 3.01651 12.201C3.03214 12.3924 3.06007 12.484 3.09083 12.5443C3.17072 12.7011 3.29821 12.8286 3.45501 12.9085C3.51538 12.9393 3.6069 12.9672 3.79832 12.9828C3.99557 12.999 4.25171 12.9993 4.63333 12.9993H11.3667C11.7483 12.9993 12.0044 12.999 12.2017 12.9828C12.3931 12.9672 12.4846 12.9393 12.545 12.9085C12.7018 12.8286 12.8293 12.7011 12.9092 12.5443C12.9399 12.484 12.9679 12.3924 12.9835 12.201C12.9996 12.0038 13 11.7476 13 11.366V8.49935C13 8.22321 13.2239 7.99935 13.5 7.99935C13.7761 7.99935 14 8.22321 14 8.49935V11.3863C14 11.7424 14 12.0396 13.9802 12.2825C13.9595 12.5357 13.9147 12.7735 13.8002 12.9983C13.6244 13.3433 13.3439 13.6238 12.999 13.7995C12.7741 13.9141 12.5364 13.9588 12.2831 13.9795C12.0403 13.9994 11.7431 13.9994 11.387 13.9993H4.61303C4.25693 13.9994 3.95971 13.9994 3.71689 13.9795C3.46363 13.9588 3.22586 13.9141 3.00102 13.7995C2.65605 13.6238 2.37559 13.3433 2.19982 12.9983C2.08526 12.7735 2.04052 12.5357 2.01983 12.2825C1.99999 12.0396 1.99999 11.7424 2 11.3863V4.61239C1.99999 4.25629 1.99999 3.95906 2.01983 3.71624C2.04052 3.46298 2.08526 3.22521 2.19982 3.00037C2.37559 2.6554 2.65605 2.37494 3.00102 2.19917C3.22586 2.08461 3.46363 2.03987 3.71689 2.01918C3.95971 1.99934 4.25694 1.99934 4.61304 1.99935Z"
fill="currentColor"
/>
<path
d="M13.963 1.92174C13.2471 1.20578 12.0863 1.20578 11.3703 1.92174L5.67504 7.617C5.45625 7.83579 5.33333 8.13254 5.33333 8.44196V10.1658C5.33333 10.442 5.55719 10.6658 5.83333 10.6658H7.55719C7.86661 10.6658 8.16336 10.5429 8.38215 10.3241L14.0774 4.62884C14.7934 3.91288 14.7934 2.75208 14.0774 2.03612L13.963 1.92174Z"
fill="currentColor"
/>
</g>
</CustomIcon>
);
};
@@ -1,18 +0,0 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const HideEyeOffIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M2.02012 1.31246C1.82486 1.1172 1.50828 1.1172 1.31301 1.31246C1.11775 1.50772 1.11775 1.82431 1.31301 2.01957L2.02012 1.31246ZM1.62744 7.42685L1.1887 7.18704L1.62744 7.42685ZM11.7196 11.719L12.0731 11.3655L11.7196 11.719ZM13.9797 14.6862C14.1749 14.8815 14.4915 14.8815 14.6868 14.6862C14.8821 14.491 14.8821 14.1744 14.6868 13.9791L13.9797 14.6862ZM1.62804 8.57302L1.18932 8.81287H1.18932L1.62804 8.57302ZM5.2498 3.12216C4.99958 3.23898 4.89145 3.53652 5.00828 3.78674C5.1251 4.03695 5.42264 4.14508 5.67286 4.02826L5.2498 3.12216ZM14.3718 7.42567L14.8105 7.18582L14.3718 7.42567ZM12.3668 10.4904C12.1743 10.6884 12.1786 11.0049 12.3766 11.1975C12.5745 11.39 12.891 11.3857 13.0836 11.1877L12.3668 10.4904ZM14.3724 8.57183L13.9336 8.33203V8.33203L14.3724 8.57183ZM6.58569 6.58512L6.93924 6.23157C6.74398 6.03631 6.4274 6.03631 6.23213 6.23157L6.58569 6.58512ZM9.41411 9.41355L9.76767 9.7671C9.86144 9.67334 9.91411 9.54616 9.91411 9.41355C9.91411 9.28094 9.86144 9.15377 9.76767 9.06L9.41411 9.41355ZM1.31301 2.01957L3.92667 4.63323L4.63378 3.92612L2.02012 1.31246L1.31301 2.01957ZM2.06618 7.66665C2.78183 6.35732 3.64551 5.3672 4.57648 4.68246L3.98397 3.87689C2.91986 4.65956 1.96456 5.76756 1.1887 7.18704L2.06618 7.66665ZM3.92667 4.63323L11.366 12.0726L12.0731 11.3655L4.63378 3.92612L3.92667 4.63323ZM11.366 12.0726L13.9797 14.6862L14.6868 13.9791L12.0731 11.3655L11.366 12.0726ZM1.18932 8.81287C2.47954 11.1728 4.26886 12.674 6.21987 13.243C8.17733 13.8138 10.2399 13.428 12.0158 12.1218L11.4233 11.3162C9.88108 12.4506 8.13835 12.7608 6.49983 12.283C4.85485 11.8033 3.25748 10.5112 2.06675 8.33317L1.18932 8.81287ZM1.1887 7.18704C0.911724 7.69378 0.912792 8.30707 1.18932 8.81287L2.06675 8.33317C1.95313 8.12535 1.95308 7.87358 2.06618 7.66665L1.1887 7.18704ZM5.67286 4.02826C7.09829 3.36272 8.63166 3.32591 10.0669 3.91426C11.5069 4.50457 12.8795 5.7385 13.933 7.66553L14.8105 7.18582C13.6688 5.09759 12.1371 3.68216 10.4462 2.98899C8.75047 2.29385 6.92622 2.33943 5.2498 3.12216L5.67286 4.02826ZM13.0836 11.1877C13.7227 10.5308 14.305 9.7376 14.8111 8.81164L13.9336 8.33203C13.4669 9.18586 12.9371 9.90419 12.3668 10.4904L13.0836 11.1877ZM13.933 7.66553C14.0467 7.87336 14.0467 8.12511 13.9336 8.33203L14.8111 8.81164C15.0881 8.30489 15.087 7.69161 14.8105 7.18582L13.933 7.66553ZM7.9999 9.49934C7.17147 9.49934 6.4999 8.82776 6.4999 7.99934H5.4999C5.4999 9.38005 6.61919 10.4993 7.9999 10.4993V9.49934ZM6.4999 7.99934C6.4999 7.58499 6.66725 7.21067 6.93924 6.93868L6.23213 6.23157C5.78027 6.68343 5.4999 7.30912 5.4999 7.99934H6.4999ZM6.23213 6.93868L9.06056 9.7671L9.76767 9.06L6.93924 6.23157L6.23213 6.93868ZM9.06056 9.06C8.78857 9.33199 8.41425 9.49934 7.9999 9.49934V10.4993C8.69012 10.4993 9.3158 10.219 9.76767 9.7671L9.06056 9.06Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -1,18 +0,0 @@
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const ShowEyeIcon: React.FC<CustomIconProps> = (props) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
{...props}
>
<path
d="M1.62774 7.42694L2.06646 7.66677L1.62774 7.42694ZM14.3721 7.42686L13.9333 7.66669V7.66669L14.3721 7.42686ZM1.62774 8.5731L2.06646 8.33327H2.06646L1.62774 8.5731ZM14.3721 8.57302L14.8108 8.81285L14.8108 8.81285L14.3721 8.57302ZM2.06646 7.66677C3.61474 4.83447 5.84609 3.50002 7.99991 3.5C10.1537 3.49998 12.3851 4.8344 13.9333 7.66669L14.8108 7.18703C13.1325 4.11679 10.6049 2.49998 7.9999 2.5C5.39486 2.50002 2.86735 4.11687 1.18901 7.18711L2.06646 7.66677ZM1.18901 8.81293C2.86735 11.8832 5.39486 13.5 7.99991 13.5C10.6049 13.4999 13.1325 11.8831 14.8108 8.81285L13.9333 8.33319C12.3851 11.1655 10.1537 12.4999 7.9999 12.5C5.84609 12.5 3.61473 11.1656 2.06646 8.33327L1.18901 8.81293ZM1.18901 7.18711C0.912257 7.69338 0.912257 8.30665 1.18901 8.81293L2.06646 8.33327C1.95311 8.1259 1.95311 7.87414 2.06646 7.66677L1.18901 7.18711ZM13.9333 7.66669C14.0467 7.87406 14.0467 8.12582 13.9333 8.33319L14.8108 8.81285C15.0875 8.30658 15.0875 7.69331 14.8108 7.18703L13.9333 7.66669ZM9.4999 8C9.4999 8.82843 8.82833 9.5 7.9999 9.5V10.5C9.38062 10.5 10.4999 9.38071 10.4999 8H9.4999ZM7.9999 9.5C7.17148 9.5 6.4999 8.82843 6.4999 8H5.4999C5.4999 9.38071 6.61919 10.5 7.9999 10.5V9.5ZM6.4999 8C6.4999 7.17157 7.17148 6.5 7.9999 6.5V5.5C6.61919 5.5 5.4999 6.61929 5.4999 8H6.4999ZM7.9999 6.5C8.82833 6.5 9.4999 7.17157 9.4999 8H10.4999C10.4999 6.61929 9.38062 5.5 7.9999 5.5V6.5Z"
fill="currentColor"
/>
</CustomIcon>
);
};
@@ -10,8 +10,8 @@ export const TrashIcon: React.FC<CustomIconProps> = (props) => {
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.05612 3.33398C5.52062 2.16265 6.66341 1.33398 8.00079 1.33398C9.33816 1.33398 10.481 2.16265 10.9455 3.33398H14.1668C14.443 3.33398 14.6668 3.55784 14.6668 3.83398C14.6668 4.11013 14.443 4.33398 14.1668 4.33398H13.3023L12.7463 12.952C12.684 13.9167 11.8834 14.6673 10.9167 14.6673H5.08358C4.11688 14.6673 3.31629 13.9167 3.25405 12.952L2.69805 4.33398H1.8335C1.55735 4.33398 1.3335 4.11013 1.3335 3.83398C1.3335 3.55784 1.55735 3.33398 1.8335 3.33398H5.05612ZM6.17457 3.33398C6.55973 2.73248 7.23408 2.33398 8.00079 2.33398C8.76749 2.33398 9.44184 2.73248 9.827 3.33398H6.17457ZM7.00016 7.16732C7.00016 6.89118 6.77631 6.66732 6.50016 6.66732C6.22402 6.66732 6.00016 6.89118 6.00016 7.16732V10.834C6.00016 11.1101 6.22402 11.334 6.50016 11.334C6.77631 11.334 7.00016 11.1101 7.00016 10.834V7.16732ZM9.50016 6.66732C9.77631 6.66732 10.0002 6.89118 10.0002 7.16732V10.834C10.0002 11.1101 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.1101 9.00016 10.834V7.16732C9.00016 6.89118 9.22402 6.66732 9.50016 6.66732Z"
fill="currentColor"
/>
@@ -73,9 +73,6 @@ export * from './SwitchIcon';
export * from './CollaboratorsIcon';
export * from './ChevronUpSmallIcon';
export * from './ChevronDownSmallIcon';
export * from './EditBigIcon';
export * from './ShowEyeIcon';
export * from './HideEyeOffIcon';
// Templates
export * from './templates';
@@ -4,6 +4,7 @@ import {
useMemo,
ComponentPropsWithoutRef,
} from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
@@ -11,7 +12,7 @@ import { cn } from 'utils/classnames';
import { InputTheme, inputTheme } from './Input.theme';
export interface InputProps
export interface InputProps<T extends FieldValues = FieldValues>
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
@@ -19,6 +20,9 @@ export interface InputProps
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
// react-hook-form optional register
register?: ReturnType<UseFormRegister<T>>;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
@@ -30,6 +34,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
leftIcon,
rightIcon,
helperText,
register,
size,
state,
appearance,
@@ -102,11 +107,12 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
{...(register ? register : {})}
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
ref={ref}
{...props}
ref={ref}
/>
{rightIcon && renderRightIcon}
</div>
@@ -20,7 +20,6 @@ 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;
@@ -84,7 +83,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]);
const handleLogOut = useCallback(async () => {
await fetch(`${baseUrl}/auth/logout`, {
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
method: 'POST',
credentials: 'include',
});
@@ -103,12 +103,11 @@ export const UserSelect = ({ options, value }: UserSelectProps) => {
<div className={theme.container()}>
{/* Input */}
<div
{...getToggleButtonProps(
{
ref: inputWrapperRef,
},
{ suppressRefError: true },
)}
{...getToggleButtonProps({
ref: inputWrapperRef,
suppressRefError: true,
})}
ref={inputWrapperRef}
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,117 +0,0 @@
import { useEffect, useRef } from 'react';
import { Input } from '../Input';
export interface VerifyCodeInputProps {
code: string;
setCode: (code: string) => void;
submitCode: () => void;
loading: boolean;
}
export const VerifyCodeInput = ({
code,
setCode,
submitCode,
loading,
}: VerifyCodeInputProps) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>,
i: number,
) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
if (pasteData.length > 0) {
let newCodeArray = code.split('');
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
newCodeArray[i + j] = pasteData[j];
}
const newCode = newCodeArray.join('');
setCode(newCode);
const nextIndex = Math.min(i + pasteData.length, 5);
const nextInput = inputRefs.current[nextIndex];
if (nextInput) nextInput.focus();
if (!newCode.includes(' ')) {
submitCode();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
i: number,
) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (e.key === 'Backspace') {
e.preventDefault();
const isEmpty = code[i] === ' ';
const newCode = !isEmpty
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
setCode(newCode.slice(0, 6));
if (i === 0 || !isEmpty) return;
const prev = inputRefs.current[i - 1];
if (prev) prev.focus();
return;
}
if (!e.key.match(/[0-9]/)) return;
e.preventDefault(); // Prevent the default event to avoid duplicate input
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
setCode(newCode);
if (i === 5) {
submitCode();
return;
}
const next = inputRefs.current[i + 1];
if (next) next.focus();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
const value = e.target.value.slice(-1);
if (!value.match(/[0-9]/)) return;
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
setCode(newCode);
if (i < 5) {
const next = inputRefs.current[i + 1];
if (next) next.focus();
}
if (!newCode.includes(' ')) {
submitCode();
}
};
useEffect(() => {
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
}, []);
return (
<div className="flex gap-2">
{code.split('').map((char, i) => (
<Input
key={i}
value={char === ' ' ? '' : char}
ref={(el) => (inputRefs.current[i] = el)}
onChange={(e) => handleChange(e, i)}
onPaste={(e) => handlePaste(e, i)}
onKeyDown={(e) => handleKeyDown(e, i)}
disabled={!!loading}
style={{ textAlign: 'center' }} // Add this line to center text
/>
))}
</div>
);
};
@@ -1 +0,0 @@
export { VerifyCodeInput } from './VerifyCodeInput';
@@ -1,27 +0,0 @@
export { Avatar } from './Avatar';
export { Badge } from './Badge';
export { Button } from './Button';
export { Calendar } from './Calendar';
export { Checkbox } from './Checkbox';
export { DatePicker } from './DatePicker';
export { DotBorder } from './DotBorder';
export { Heading } from './Heading';
export { IconWithFrame } from './IconWithFrame';
export { InlineNotification } from './InlineNotification';
export { Input } from './Input';
export { Modal } from './Modal';
export { OverflownText } from './OverflownText';
export { Radio } from './Radio';
export { SegmentedControls } from './SegmentedControls';
export { Select } from './Select';
export { Sidebar } from './Sidebar';
export { Step } from './Steps';
export { Switch } from './Switch';
export { Table } from './Table';
export { Tabs } from './Tabs';
export { Tag } from './Tag';
export { useToast } from './Toast';
export { Tooltip } from './Tooltip';
export { UserSelect } from './UserSelect';
export { VerifyCodeInput } from './VerifyCodeInput';
export { WavyBorder } from './WavyBorder';
@@ -1,11 +1,9 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient();
if (!VITE_WALLET_CONNECT_ID) {
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
}
+5 -5
View File
@@ -14,16 +14,16 @@ 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';
console.log(`v-0.0.9`);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
assert(
import.meta.env.VITE_SERVER_URL,
'REACT_APP_SERVER_URL is not set in env',
);
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint });
@@ -32,7 +32,7 @@ const ProjectSearch = () => {
return (
<section className="h-full flex flex-col">
{/* Header */}
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] hover:z-30">
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] z-30">
<div className="flex pr-6 pl-2 py-2 items-center">
<div className="flex-1">
<ProjectSearchBar
@@ -1,99 +0,0 @@
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 { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
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);
// add a pause for ux
await new Promise((resolve) => setTimeout(resolve, 250));
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.trim().length === 5;
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">
<VerifyCodeInput
loading={!!loading}
code={accessCode}
setCode={setAccessCode}
submitCode={validateAccessCode}
/>
</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}.{' '}
<a href="/signup" className="underline">
Try again?
</a>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -1,20 +0,0 @@
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,9 +47,7 @@ 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,7 +21,6 @@ import {
turnkeySignin,
turnkeySignup,
} from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -46,9 +45,7 @@ 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 });
@@ -112,29 +109,6 @@ 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,15 +1,14 @@
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: React.FC = () => {
export const SnowballAuth = () => {
const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup',
@@ -31,7 +30,7 @@ export const SnowballAuth: React.FC = () => {
if (screen === 'signup') {
return (
<AccessSignUp
<SignUp
onDone={() => {
setScreen('success');
}}
@@ -1,19 +1,5 @@
import { Heading } from 'components/shared/Heading';
const Settings = () => {
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
<Heading as="h2" className="text-[24px]">
Settings
</Heading>
</div>
</div>
</div>
</section>
);
return <div className="p-5">Settings page</div>;
};
export default Settings;
@@ -1,11 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ProjectCard } from 'components/projects/ProjectCard';
import { Heading, Badge, Button } from 'components/shared';
import { PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon';
import { ProjectCard } from 'components/projects/ProjectCard';
import { Heading } from 'components/shared/Heading';
import { Badge } from 'components/shared/Badge';
import { useGQLClient } from 'context/GQLClientContext';
const Projects = () => {
const client = useGQLClient();
@@ -26,7 +28,6 @@ const Projects = () => {
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
@@ -11,128 +11,98 @@ import {
import { Heading } from 'components/shared/Heading';
import logoAnimation from 'components/../../public/lottie/logo.json';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useEffect, useState } from 'react';
const Id = () => {
const { id, orgSlug } = useParams();
const client = useGQLClient();
const [project, setProject] = useState<Project | null>(null);
const handleSetupDomain = async () => {
if (id) {
// console.log('id', id);
// console.log('getting project for id', id);
const project = await client.getProject(id);
// console.log('project found:', project);
if (project && project.project) {
// console.log('project:', project.project);
setProject(project.project);
}
} else {
window.location.href = '/';
}
const handleSetupDomain = () => {
//TODO: Implement this
};
useEffect(() => {
handleSetupDomain();
});
return (
<>
{project ? (
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
{/* Icon */}
<div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
</div>
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
{/* Icon */}
<div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
</div>
{/* Heading */}
<div className="flex flex-col items-center gap-1.5">
<Heading as="h3" className="font-medium text-xl">
Project deployed successfully.
</Heading>
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
Your project has been deployed at{' '}
<Button
className="no-underline text-elements-link"
// TODO: use dynamic value
href={project ? `https://${project.subDomain}` : ''}
as="a"
variant="link-emphasized"
external
leftIcon={<LinkChainIcon />}
>
{project.subDomain}
{/* Heading */}
<div className="flex flex-col items-center gap-1.5">
<Heading as="h3" className="font-medium text-xl">
Project deployed successfully.
</Heading>
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
Your project has been deployed at{' '}
<Button
className="no-underline text-elements-link"
// TODO: use dynamic value
href="https://www.iglootools.snowballtools.xyz"
as="a"
variant="link-emphasized"
external
leftIcon={<LinkChainIcon />}
>
{/* // TODO: use dynamic value */}
www.iglootools.snowballtools.xyz
</Button>
</p>
</div>
{/* Card */}
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
{/* Trigger question */}
<div className="flex gap-2 justify-center items-center py-3">
<div className="h-5 w-5">
<QuestionMarkRoundFilledIcon size={18} />
</div>
<Heading as="h5" className="font-sans font-medium text-sm">
{`Wondering what's next?`}
</Heading>
</div>
{/* CTA card */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
<div className="flex gap-2">
<Badge variant="secondary">1</Badge>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<Heading as="h6" className="text-sm font-sans">
Add a custom domain
</Heading>
<p className="text-xs text-elements-low-em font-sans">
Make it easy for your visitors to remember your URL with a
custom domain.
</p>
</div>
<Button onClick={handleSetupDomain} variant="tertiary" size="sm">
Setup domain
</Button>
</p>
</div>
{/* Card */}
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
{/* Trigger question */}
<div className="flex gap-2 justify-center items-center py-3">
<div className="h-5 w-5">
<QuestionMarkRoundFilledIcon size={18} />
</div>
<Heading as="h5" className="font-sans font-medium text-sm">
{`Wondering what's next?`}
</Heading>
</div>
{/* CTA card */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
<div className="flex gap-2">
<Badge variant="secondary">1</Badge>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<Heading as="h6" className="text-sm font-sans">
Add a custom domain
</Heading>
<p className="text-xs text-elements-low-em font-sans">
Make it easy for your visitors to remember your URL with a
custom domain.
</p>
</div>
<Button
onClick={handleSetupDomain}
variant="tertiary"
size="sm"
>
Setup domain
</Button>
</div>
</div>
</div>
</div>
{/* CTA Buttons */}
<div className="flex flex-col lg:flex-row justify-center gap-3">
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
</div>
</div>
</div>
) : (
<></>
)}
</>
</div>
{/* CTA Buttons */}
<div className="flex flex-col lg:flex-row justify-center gap-3">
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
</div>
</div>
</div>
);
};
@@ -11,10 +11,7 @@ import { Template } from '../../../../../types/types';
import { Heading } from 'components/shared/Heading';
import { Input } from 'components/shared/Input';
import { Select, SelectOption } from 'components/shared/Select';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
import { Checkbox } from 'components/shared/Checkbox';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
@@ -53,22 +50,36 @@ const CreateRepo = () => {
assert(template.repoFullName, 'Template URL not provided');
const [owner, repo] = template.repoFullName.split('/');
setIsLoading(true);
// TODO: Handle this functionality in backend
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
template_owner: owner,
template_repo: repo,
owner: data.account,
name: data.repoName,
include_all_branches: false,
private: data.isPrivate,
});
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug!,
{
templateOwner: owner,
templateRepo: repo,
owner: data.account,
name: data.repoName,
isPrivate: false,
},
);
if (!gitRepo) {
return;
}
navigate(
`deploy?projectId=${addProjectFromTemplate.id}&templateId=${template.id}`,
);
// Refetch to always get correct default branch
const templateRepo = await octokit.rest.repos.get({
owner: template.repoFullName.split('/')[0],
repo: template.repoFullName.split('/')[1],
});
const prodBranch = templateRepo.data.default_branch ?? 'main';
const { addProject } = await client.addProject(orgSlug!, {
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
prodBranch,
repository: gitRepo.data.full_name,
// TODO: Set selected template
template: 'webapp',
});
navigate(`deploy?projectId=${addProject.id}&templateId=${template.id}`);
} catch (err) {
setIsLoading(false);
@@ -88,8 +99,8 @@ const CreateRepo = () => {
console.error((err as Error).message);
toast({
id: 'error-deploying-project',
title: 'Error deploying project',
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
@@ -195,13 +206,7 @@ const CreateRepo = () => {
{...buttonSize}
type="submit"
disabled={!Boolean(template.repoFullName) || isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
rightIcon={<ArrowRightCircleFilledIcon />}
>
Deploy
</Button>
@@ -1,11 +1,14 @@
import { useEffect, useState } from 'react';
import { Domain, DomainStatus } from 'gql-client';
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { RequestError } from 'octokit';
import { useOctokit } from 'context/OctokitContext';
import { useOctokit } from '../../../../context/OctokitContext';
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Avatar, Tag } from 'components/shared';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { Button } from 'components/shared/Button';
import { Heading } from 'components/shared/Heading';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials';
import {
BranchStrokeIcon,
@@ -15,12 +18,12 @@ import {
GithubStrokeIcon,
GlobeIcon,
LinkIcon,
CalendarDaysIcon,
} from 'components/shared/CustomIcon';
import { Tag } from 'components/shared/Tag';
import { Activity } from 'components/projects/project/overview/Activity';
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
import { relativeTimeMs } from 'utils/time';
import { Domain, DomainStatus } from 'gql-client';
const COMMITS_PER_PAGE = 4;
@@ -128,12 +131,9 @@ const OverviewTabPanel = () => {
<Heading className="text-lg leading-6 font-medium truncate">
{project.name}
</Heading>
<a
href={`https://${project.subDomain}`}
className="text-sm text-elements-low-em tracking-tight truncate"
>
<p className="text-sm text-elements-low-em tracking-tight truncate">
{project.subDomain}
</a>
</p>
</div>
</div>
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
@@ -16,7 +16,7 @@ const FIRST_MEMBER_CARD = 0;
const CollaboratorsTabPanel = () => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const { toast } = useToast();
const { project } = useOutletContext<OutletContextType>();
const [addmemberDialogOpen, setAddMemberDialogOpen] = useState(false);
@@ -39,14 +39,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_added',
title: 'Member added to project',
variant: 'success',
onDismiss: dismiss,
onDismiss() {},
});
} else {
toast({
id: 'member_not_added',
title: 'Invitation not sent',
variant: 'error',
onDismiss: dismiss,
onDismiss() {},
});
}
},
@@ -63,14 +63,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_removed',
title: 'Member removed from project',
variant: 'success',
onDismiss: dismiss,
onDismiss() {},
});
} else {
toast({
id: 'member_not_removed',
title: 'Not able to remove member',
variant: 'error',
onDismiss: dismiss,
onDismiss() {},
});
}
};
@@ -86,14 +86,14 @@ const CollaboratorsTabPanel = () => {
id: 'member_permission_updated',
title: 'Project member permission updated',
variant: 'success',
onDismiss: dismiss,
onDismiss() {},
});
} else {
toast({
id: 'member_permission_not_updated',
title: 'Project member permission not updated',
variant: 'error',
onDismiss: dismiss,
onDismiss() {},
});
}
},
@@ -1,12 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useParams } from 'react-router-dom';
import { Environment, EnvironmentVariable } from 'gql-client';
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types';
import HorizontalLine from 'components/HorizontalLine';
import { Heading } from 'components/shared/Heading';
@@ -15,13 +17,10 @@ import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon';
import { InlineNotification } from 'components/shared/InlineNotification';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
import { Environment, EnvironmentVariable } from 'gql-client';
export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams();
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [environmentVariables, setEnvironmentVariables] = useState<
EnvironmentVariable[]
@@ -119,25 +118,13 @@ export const EnvironmentVariablesTabPanel = () => {
fetchEnvironmentVariables(id);
toast({
id:
createFormData.variables.length > 1
? 'env_variable_added'
: 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
toast.success(
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
);
} else {
toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Environment variables not added');
}
},
[id, client],
@@ -166,7 +166,7 @@ const GeneralTabPanel = () => {
setSelectedTransferOrganization(org.org);
setOpenTransferDialog(!openTransferDialog);
})}
className="self-stretch space-y-3"
className="self-stretch space-y-3 px-2"
>
<Heading className="text-sky-950 text-lg font-medium leading-normal">
Transfer project
@@ -197,7 +197,7 @@ const GeneralTabPanel = () => {
from={project.organization.name}
to={selectedUserOrgName}
/>
<div className="self-stretch space-y-3">
<div className="self-stretch space-y-3 px-2">
<Heading className="text-sky-950 text-lg font-medium leading-normal">
Delete project
</Heading>
@@ -1,12 +1,12 @@
import toast from 'react-hot-toast';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../../../../context/GQLClientContext';
import { Table } from 'components/shared/Table';
import { Button } from 'components/shared/Button';
import { InlineNotification } from 'components/shared/InlineNotification';
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
const Config = () => {
const { id, orgSlug } = useParams();
@@ -14,26 +14,15 @@ const Config = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const primaryDomainName = searchParams.get('name');
const { toast, dismiss } = useToast();
const handleSubmitDomain = async () => {
if (primaryDomainName === null) {
toast({
id: 'unresolvable_domain_name',
title: 'Cannot resolve domain name',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Cannot resolve domain name');
return;
}
if (id === undefined) {
toast({
id: 'domain_cannot_find_project',
title: 'Cannot find project',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Cannot find project');
return;
}
@@ -42,20 +31,10 @@ const Config = () => {
});
if (isAdded) {
toast({
id: 'domain_added_successfully',
title: 'Domain added successfully',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Domain added successfully');
navigate(`/${orgSlug}/projects/${id}/settings/domains`);
} else {
toast({
id: 'generic_error_adding_domain',
title: 'Error adding domaint',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Error adding domain');
}
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronDownSmallIcon> = {
title: 'Icons/ChevronDownSmallIcon',
component: ChevronDownSmallIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronRight> = {
title: 'Icons/ChevronRight',
component: ChevronRight,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronUpDown> = {
title: 'Icons/ChevronUpDown',
component: ChevronUpDown,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -3,16 +3,12 @@ import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpSmallIcon> = {
title: 'Icons/ChevronUpSmallIcon',
title: 'Components/ChevronUpSmallIcon',
component: ChevronUpSmallIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { CollaboratorsIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof CollaboratorsIcon> = {
title: 'Icons/CollaboratorsIcon',
title: 'Components/CollaboratorsIcon',
component: CollaboratorsIcon,
tags: ['autodocs'],
argTypes: {
@@ -1,29 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { EditBigIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof EditBigIcon> = {
title: 'Icons/EditBigIcon',
component: EditBigIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<typeof EditBigIcon>;
export const Default: Story = {
render: ({ size, name }) => <EditBigIcon size={size} name={name} />,
args: {
size: '24px',
name: 'plus',
},
};
@@ -1,29 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { HideEyeOffIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof HideEyeOffIcon> = {
title: 'Icons/HideEyeOffIcon',
component: HideEyeOffIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<typeof HideEyeOffIcon>;
export const Default: Story = {
render: ({ size, name }) => <HideEyeOffIcon size={size} name={name} />,
args: {
size: '24px',
name: 'plus',
},
};
@@ -1,29 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ShowEyeIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ShowEyeIcon> = {
title: 'Icons/ShowEyeIcon',
component: ShowEyeIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
},
};
export default meta;
type Story = StoryObj<typeof ShowEyeIcon>;
export const Default: Story = {
render: ({ size, name }) => <ShowEyeIcon size={size} name={name} />,
args: {
size: '24px',
name: 'plus',
},
};
@@ -1,20 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
const meta: Meta<typeof ProjectSearchBar> = {
title: 'Components/ProjectSearchBar',
component: ProjectSearchBar,
tags: ['autodocs'],
argTypes: {
onChange: {
action: 'change',
},
},
};
export default meta;
type Story = StoryObj<typeof ProjectSearchBar>;
export const Default: Story = {};
@@ -1,125 +0,0 @@
import {
User,
Project,
Organization,
Role,
OrganizationMember,
ProjectMember,
EnvironmentVariable,
Deployment,
DeploymentStatus,
DomainStatus,
Domain,
Environment,
Permission,
} from 'gql-client';
export const user: User = {
id: '1',
email: 'helloworld@helloworld.com',
isVerified: true,
createdAt: '2021-08-01T00:00:00.000Z',
name: 'Hello World',
updatedAt: '2021-08-01T00:00:00.000Z',
gitHubToken: 'GitHub Token',
};
export const organizationMember: OrganizationMember = {
id: '1',
member: user,
role: Role.Owner,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
};
export const organization: Organization = {
id: '1',
name: 'Organization',
slug: 'organization',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
members: [organizationMember],
projects: [],
};
export const member: ProjectMember = {
id: '1',
member: user,
permissions: [Permission.Edit],
isPending: false,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
};
export const environmentVariable0: EnvironmentVariable = {
id: '1',
key: 'API_KEY',
value: '123456',
environment: Environment.Development,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
};
export const environmentVariable1: EnvironmentVariable = {
id: '2',
key: 'API_KEY_2',
value: '123456',
environment: Environment.Development,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
};
export const domain0: Domain = {
id: '1',
name: 'domain.com',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
status: DomainStatus.Live,
redirectTo: null,
};
export const domain1: Domain = {
id: '2',
name: 'www.domain.com',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
status: DomainStatus.Live,
redirectTo: domain0,
};
export const deployment0: Deployment = {
id: '1',
url: 'https://deployment.com',
status: DeploymentStatus.Ready,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
environment: Environment.Development,
isCurrent: true,
commitHash: 'Commit Hash',
domain: domain0,
commitMessage: 'Commit Message',
createdBy: user,
};
export const project: Project = {
id: '1',
name: 'GithubUsername-ProjectName',
owner: user,
deployments: [deployment0],
repository: 'Repository',
prodBranch: 'Prod Branch',
description: 'Description',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
framework: 'NextJS',
environmentVariables: [environmentVariable0, environmentVariable1],
organization: organization,
template: 'Template',
members: [member],
webhooks: ['beepboop'],
icon: 'Icon',
subDomain: 'SubDomain',
};
@@ -1,31 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import AddMemberDialog from 'components/projects/project/settings/AddMemberDialog';
const meta: Meta<typeof AddMemberDialog> = {
title: 'Project/Settings/AddMemberDialog',
component: AddMemberDialog,
tags: ['autodocs'],
argTypes: {
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
handleAddMember: {
action: 'addMember',
},
},
args: {
open: true,
},
};
export default meta;
type Story = StoryObj<typeof AddMemberDialog>;
export const Default: Story = {};
@@ -1,50 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
reactRouterParameters,
withRouter,
} from 'storybook-addon-remix-react-router';
import DeleteProjectDialog from 'components/projects/project/settings/DeleteProjectDialog';
import { project } from '../../MockStoriesData';
const meta: Meta<typeof DeleteProjectDialog> = {
title: 'Project/Settings/DeleteProjectDialog',
component: DeleteProjectDialog,
tags: ['autodocs'],
decorators: [withRouter],
parameters: {
reactRouter: reactRouterParameters({
location: {
pathParams: { userId: 'me' },
},
routing: {
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
},
}),
},
argTypes: {
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
project: {
control: {
type: 'object',
},
},
},
args: {
open: true,
project: project,
},
};
export default meta;
type Story = StoryObj<typeof DeleteProjectDialog>;
export const Default: Story = {};
@@ -1,38 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import {
environmentVariable0,
environmentVariable1,
} from '../../MockStoriesData';
import { Environment } from 'gql-client';
const meta: Meta<typeof DisplayEnvironmentVariables> = {
title: 'Project/Settings/DisplayEnvironmentVariables',
component: DisplayEnvironmentVariables,
argTypes: {
environment: {
control: {
type: 'object',
},
},
variables: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
args: {
environment: Environment.Development,
variables: [environmentVariable0, environmentVariable1],
},
};
export default meta;
type Story = StoryObj<typeof DisplayEnvironmentVariables>;
export const Default: Story = {};
@@ -1,48 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import DomainCard from 'components/projects/project/settings/DomainCard';
import { domain0, domain1, project } from '../../MockStoriesData';
const meta: Meta<typeof DomainCard> = {
title: 'Project/Settings/DomainCard',
component: DomainCard,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
project: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof DomainCard>;
export const Default: Story = {
args: {
domains: [domain0, domain1],
domain: domain0,
branches: ['main'],
project: project,
},
};
@@ -1,43 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import EditDomainDialog from 'components/projects/project/settings/EditDomainDialog';
const meta: Meta<typeof EditDomainDialog> = {
title: 'Components/EditDomainDialog',
component: EditDomainDialog,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof EditDomainDialog>;
export const Default: Story = {};
@@ -1,28 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import EditEnvironmentVariableRow from 'components/projects/project/settings/EditEnvironmentVariableRow';
import { environmentVariable0 } from '../../MockStoriesData';
const meta: Meta<typeof EditEnvironmentVariableRow> = {
title: 'Project/Settings/EditEnvironmentVariableRow',
component: EditEnvironmentVariableRow,
argTypes: {
variable: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
args: {
variable: environmentVariable0,
},
};
export default meta;
type Story = StoryObj<typeof EditEnvironmentVariableRow>;
export const Default: Story = {};

Some files were not shown because too many files have changed in this diff Show More