forked from cerc-io/snowballtools-base
Compare commits
146
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcaeabf07d | ||
|
|
1e056d7969 | ||
|
|
6d8723881c | ||
|
|
d387229e80 | ||
|
|
54c2e8ec0b | ||
|
|
410def0750 | ||
|
|
d181e6ba98 | ||
|
|
100e03b7a4 | ||
|
|
5d0dd93271 | ||
|
|
3bca06b590 | ||
|
|
c950bd8d93 | ||
|
|
b7fe301c81 | ||
|
|
3cf8769259 | ||
|
|
35bc69bb15 | ||
|
|
5451c3be6f | ||
|
|
5f8afab020 | ||
|
|
0bcb7ca4d6 | ||
|
|
cfeb85f5c4 | ||
|
|
705516f5e3 | ||
|
|
effa0bbf14 | ||
|
|
10a875d6e8 | ||
|
|
284eb1403a | ||
|
|
538e89e3c9 | ||
|
|
b88752da69 | ||
|
|
48c569c371 | ||
|
|
549dc5af10 | ||
|
|
012beaad57 | ||
|
|
81f27442fb | ||
|
|
bdb948f69e | ||
|
|
2442e92395 | ||
|
|
dcf271a01b | ||
|
|
48fd953c60 | ||
|
|
5eff273abb | ||
|
|
f9af066333 | ||
|
|
04460a24ac | ||
|
|
ccc4589033 | ||
|
|
cf2f9a88e1 | ||
|
|
50749055bf | ||
|
|
ef7c673698 | ||
|
|
7f11d8818d | ||
|
|
ece9a2ecec | ||
|
|
cb614c8d8f | ||
|
|
dbb928cd2f | ||
|
|
cfd129fb96 | ||
|
|
efc10d3d7e | ||
|
|
042aff2f87 | ||
|
|
b48afed24f | ||
|
|
0dfecd024d | ||
|
|
9db87e6be3 | ||
|
|
5a5756bcf2 | ||
|
|
22233d95f8 | ||
|
|
12fba5ee0c | ||
|
|
9b6c777f5f | ||
|
|
3718e260f5 | ||
|
|
05e1ac6b02 | ||
|
|
55848b1dd1 | ||
|
|
42bf1534fb | ||
|
|
7d1810ebd9 | ||
|
|
d2ca4df35a | ||
|
|
30bbe4d766 | ||
|
|
0e7343da6f | ||
|
|
8952393b31 | ||
|
|
32918b7930 | ||
|
|
596889b5e2 | ||
|
|
fb932eeb04 | ||
|
|
200ea3ca7b | ||
|
|
ba87c6f22a | ||
|
|
4d646e1ac0 | ||
|
|
5be29a9745 | ||
|
|
fffc370d40 | ||
|
|
1cd60d84dd | ||
|
|
c8a153ad27 | ||
|
|
bb7a88f7e9 | ||
|
|
675112079c | ||
|
|
cb928c3360 | ||
|
|
5836779403 | ||
|
|
028dd35db1 | ||
|
|
ef9f4df28a | ||
|
|
496404195c | ||
|
|
9dec48d67c | ||
|
|
267b52a352 | ||
|
|
fa96b6e734 | ||
|
|
22c581dd33 | ||
|
|
d3013719e6 | ||
|
|
98a4d7be07 | ||
|
|
3928e2610f | ||
|
|
075cc05db5 | ||
|
|
6df094bf2e | ||
|
|
2369f4498a | ||
|
|
eb6a727425 | ||
|
|
da2f7ede42 | ||
|
|
448d0ceb7c | ||
|
|
195d957251 | ||
|
|
807a4b1197 | ||
|
|
5f6723fad6 | ||
|
|
b663067035 | ||
|
|
b3a99475af | ||
|
|
ea44efa0f2 | ||
|
|
7d5963d776 | ||
|
|
33b6191539 | ||
|
|
636f68d7a4 | ||
|
|
237e9e5cb9 | ||
|
|
e70bb34190 | ||
|
|
b5eef95d15 | ||
|
|
f0121605c4 | ||
|
|
c8f1c58507 | ||
|
|
83508cd81d | ||
|
|
76c59e2c58 | ||
|
|
a5bce0cda2 | ||
|
|
3a085a79ae | ||
|
|
abfe6a523c | ||
|
|
f764bea6d1 | ||
|
|
cc9c24cca1 | ||
|
|
e482f998a1 | ||
|
|
b255078431 | ||
|
|
26260976fb | ||
|
|
5e3a6ad2b5 | ||
|
|
f31ee7349f | ||
|
|
5c5d759c10 | ||
|
|
dc6bf3794a | ||
|
|
af0d28e654 | ||
|
|
4f7f9cf914 | ||
|
|
ad7dd1920a | ||
|
|
cc97ddff9d | ||
|
|
0a5a1b8c55 | ||
|
|
4f2944a974 | ||
|
|
69198307fb | ||
|
|
0f7c6c73c9 | ||
|
|
2ac657c32e | ||
|
|
943d427db1 | ||
|
|
7e6b74208d | ||
|
|
2b78cab849 | ||
|
|
862862a9c5 | ||
|
|
6a3af491ca | ||
|
|
92f7d07f3f | ||
|
|
6fb94aeb11 | ||
|
|
1c848c5a89 | ||
|
|
17b5ff9a74 | ||
|
|
56adfc4806 | ||
|
|
b51d050ee6 | ||
|
|
8021ff7ff9 | ||
|
|
e850435c2d | ||
|
|
8a43698c43 | ||
|
|
499dba2c5e | ||
|
|
d498ba7e9a | ||
|
|
082353e9bc |
@@ -1 +1,7 @@
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
.yarnrc.yml
|
||||
.yarn/
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
@@ -28,6 +28,12 @@
|
||||
cd packages/backend
|
||||
```
|
||||
|
||||
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||
|
||||
```bash
|
||||
mv environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
- In "Homepage URL", type `http://localhost:3000`
|
||||
@@ -181,17 +187,24 @@
|
||||
cd packages/frontend
|
||||
```
|
||||
|
||||
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||
|
||||
```bash
|
||||
mv .env.example .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_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||
|
||||
```env
|
||||
# Set actual owner/name of the template repo that will be used for creating new repo
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||
```
|
||||
|
||||
### Frontend Production
|
||||
|
||||
+10
-1
@@ -9,10 +9,19 @@ if [[ -d "$DEST_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$PKG_DIR/.env" ]]; then
|
||||
echo "Using existing .env file"
|
||||
else
|
||||
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
|
||||
echo "Created .env file. Please populate with the correct values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_templaterepo'
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||
EOF
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"allowArgumentsExplicitlyTypedAsAny": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "ignoreRestSiblings": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
db
|
||||
dist
|
||||
environments/local.toml
|
||||
-5
@@ -17,11 +17,6 @@
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[gitea]
|
||||
[gitea.oAuth]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
@@ -21,7 +21,6 @@
|
||||
"luxon": "^3.4.4",
|
||||
"nanoid": "3",
|
||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||
"node-fetch": "2",
|
||||
"octokit": "^3.1.2",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"semver": "^7.6.0",
|
||||
@@ -47,7 +46,6 @@
|
||||
"devDependencies": {
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface GitHubConfig {
|
||||
oAuth: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
@@ -39,7 +39,7 @@ export interface RegistryConfig {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface MiscConfig {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import process from 'process';
|
||||
|
||||
export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||
export const DEFAULT_CONFIG_FILE_PATH =
|
||||
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||
|
||||
export const DEFAULT_GQL_PATH = '/graphql';
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
||||
import {
|
||||
DataSource,
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere
|
||||
} from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
import assert from 'assert';
|
||||
@@ -74,14 +80,18 @@ export class Database {
|
||||
return updateResult.affected > 0;
|
||||
}
|
||||
|
||||
async getOrganizations (options: FindManyOptions<Organization>): Promise<Organization[]> {
|
||||
async getOrganizations (
|
||||
options: FindManyOptions<Organization>
|
||||
): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
const organizations = await organizationRepository.find(options);
|
||||
|
||||
return organizations;
|
||||
}
|
||||
|
||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
||||
async getOrganization (
|
||||
options: FindOneOptions<Organization>
|
||||
): Promise<Organization | null> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
const organization = await organizationRepository.findOne(options);
|
||||
|
||||
@@ -123,7 +133,11 @@ export class Database {
|
||||
|
||||
const project = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
@@ -136,19 +150,29 @@ export class Database {
|
||||
return project;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.leftJoin('project.organization', 'organization')
|
||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
||||
userId,
|
||||
organizationSlug
|
||||
})
|
||||
.where(
|
||||
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||
{
|
||||
userId,
|
||||
organizationSlug
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
return projects;
|
||||
@@ -157,7 +181,9 @@ export class Database {
|
||||
/**
|
||||
* Get deployments with specified filter
|
||||
*/
|
||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
||||
async getDeployments (
|
||||
options: FindManyOptions<Deployment>
|
||||
): Promise<Deployment[]> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployments = await deploymentRepository.find(options);
|
||||
|
||||
@@ -182,7 +208,9 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
||||
async getDeployment (
|
||||
options: FindOneOptions<Deployment>
|
||||
): Promise<Deployment | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployment = await deploymentRepository.findOne(options);
|
||||
|
||||
@@ -210,8 +238,11 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMembers = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@@ -228,8 +259,12 @@ export class Database {
|
||||
return projectMembers;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
|
||||
const environmentVariables = await environmentVariableRepository.find({
|
||||
where: {
|
||||
@@ -244,9 +279,12 @@ export class Database {
|
||||
}
|
||||
|
||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
|
||||
const deleteResult = await projectMemberRepository.delete({
|
||||
id: projectMemberId
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
@@ -255,37 +293,63 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
||||
async updateProjectMemberById (
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const updateResult = await projectMemberRepository.update(
|
||||
{ id: projectMemberId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
async addProjectMember (
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
const newProjectMember = await projectMemberRepository.save(data);
|
||||
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
||||
async addEnvironmentVariables (
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const savedEnvironmentVariables =
|
||||
await environmentVariableRepository.save(data);
|
||||
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const updateResult = await environmentVariableRepository.update(
|
||||
{ id: environmentVariableId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
||||
async deleteEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable);
|
||||
const deleteResult = await environmentVariableRepository.delete({
|
||||
id: environmentVariableId
|
||||
});
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0;
|
||||
@@ -295,7 +359,8 @@ export class Database {
|
||||
}
|
||||
|
||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
const projectMemberWithProject = await projectMemberRepository.find({
|
||||
relations: {
|
||||
@@ -307,8 +372,7 @@ export class Database {
|
||||
where: {
|
||||
id: projectMemberId
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (projectMemberWithProject.length === 0) {
|
||||
throw new Error('Member does not exist');
|
||||
@@ -317,34 +381,49 @@ export class Database {
|
||||
return projectMemberWithProject[0];
|
||||
}
|
||||
|
||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
||||
async getProjectsBySearchText (
|
||||
userId: string,
|
||||
searchText: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
||||
userId,
|
||||
searchText: `%${searchText}%`
|
||||
})
|
||||
.where(
|
||||
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||
{
|
||||
userId,
|
||||
searchText: `%${searchText}%`
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeploymentById (
|
||||
deploymentId: string,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
return this.updateDeployment({ id: deploymentId }, data);
|
||||
}
|
||||
|
||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeployment (
|
||||
criteria: FindOptionsWhere<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const updateResult = await deploymentRepository.update(criteria, data);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
||||
async updateDeploymentsByProjectIds (
|
||||
projectIds: string[],
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
const updateResult = await deploymentRepository
|
||||
@@ -378,9 +457,15 @@ export class Database {
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
async updateProjectById (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const updateResult = await projectRepository.update({ id: projectId }, data);
|
||||
const updateResult = await projectRepository.update(
|
||||
{ id: projectId },
|
||||
data
|
||||
);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
@@ -427,14 +512,20 @@ export class Database {
|
||||
return domain;
|
||||
}
|
||||
|
||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
||||
async updateDomainById (
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const updateResult = await domainRepository.update({ id: domainId }, data);
|
||||
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
async getDomainsByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
const domains = await domainRepository.find({
|
||||
|
||||
@@ -27,26 +27,26 @@ export enum DeploymentStatus {
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
application: string
|
||||
config: string,
|
||||
meta: string
|
||||
type: string;
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
version:string
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string
|
||||
repository?: string[],
|
||||
app_version?: string
|
||||
repository_ref: string
|
||||
app_type: string
|
||||
version: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
license?: string;
|
||||
author?: string;
|
||||
repository?: string[];
|
||||
app_version?: string;
|
||||
repository_ref: string;
|
||||
app_type: string;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Domain {
|
||||
|
||||
@ManyToOne(() => Domain)
|
||||
@JoinColumn({ name: 'redirectToId' })
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
redirectTo!: Domain | null;
|
||||
|
||||
@Column({
|
||||
|
||||
@@ -27,8 +27,12 @@ export class Organization {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(userOrganization) => userOrganization.organization,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class Project {
|
||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||
deployments!: Deployment[];
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { User } from './User';
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
Edit = 'Edit',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -39,13 +39,17 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
projectMembers!: ProjectMember[];
|
||||
|
||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(UserOrganization) => UserOrganization.member,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[];
|
||||
}
|
||||
|
||||
@@ -31,9 +31,16 @@ export const main = async (): Promise<void> => {
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
||||
const service = new Service(
|
||||
{ gitHubConfig: gitHub, registryConfig },
|
||||
db,
|
||||
app,
|
||||
registry
|
||||
);
|
||||
|
||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
|
||||
const typeDefs = fs
|
||||
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||
.toString();
|
||||
const resolvers = await createResolvers(service);
|
||||
|
||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||
|
||||
@@ -6,7 +6,11 @@ import { DateTime } from 'luxon';
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment';
|
||||
import {
|
||||
ApplicationRecord,
|
||||
Deployment,
|
||||
ApplicationDeploymentRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
@@ -20,9 +24,13 @@ export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor (registryConfig : RegistryConfig) {
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord ({
|
||||
@@ -32,24 +40,38 @@ export class Registry {
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
appName: string,
|
||||
packageJSON: PackageJSON
|
||||
commitHash: string,
|
||||
appType: string,
|
||||
repoUrl: string
|
||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
// Use laconic-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords({
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
}, true);
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Get next version of record
|
||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
||||
const bondRecords = records.filter(
|
||||
(record: any) => record.bondId === this.registryConfig.bondId
|
||||
);
|
||||
const [latestBondRecord] = bondRecords.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
);
|
||||
const nextVersion = semverInc(
|
||||
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||
'patch'
|
||||
);
|
||||
|
||||
assert(nextVersion, 'Application record version not valid');
|
||||
|
||||
@@ -64,7 +86,12 @@ export class Registry {
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
||||
...(packageJSON.author && {
|
||||
author:
|
||||
typeof packageJSON.author === 'object'
|
||||
? JSON.stringify(packageJSON.author)
|
||||
: packageJSON.author
|
||||
}),
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
};
|
||||
|
||||
@@ -84,11 +111,29 @@ export class Registry {
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
await this.registry.setName(
|
||||
{
|
||||
cid: result.data.id,
|
||||
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
|
||||
return {
|
||||
applicationRecordId: result.data.id,
|
||||
applicationRecordData: applicationRecord
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
@@ -98,8 +143,8 @@ export class Registry {
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string,
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
@@ -125,7 +170,9 @@ export class Registry {
|
||||
env: data.environmentVariables
|
||||
}),
|
||||
meta: JSON.stringify({
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
})
|
||||
@@ -143,21 +190,34 @@ export class Registry {
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
||||
return {
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecords (
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||
const records = await this.registry.queryRecords({
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||
}, true);
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ids
|
||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationRecordId === record.attributes.application
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getCrn (appName: string): string {
|
||||
|
||||
@@ -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 { GitType } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@@ -34,7 +33,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getDeploymentsByProjectId(projectId);
|
||||
},
|
||||
|
||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
||||
environmentVariables: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string }
|
||||
) => {
|
||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||
},
|
||||
|
||||
@@ -46,14 +48,24 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.searchProjects(context.user, searchText);
|
||||
},
|
||||
|
||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
||||
domains: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
filter
|
||||
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
||||
) => {
|
||||
return service.getDomainsByProjectId(projectId, filter);
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
Mutation: {
|
||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
||||
removeProjectMember: async (
|
||||
_: any,
|
||||
{ projectMemberId }: { projectMemberId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(context.user, projectMemberId);
|
||||
} catch (err) {
|
||||
@@ -62,12 +74,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
||||
projectMemberId: string,
|
||||
data: {
|
||||
permissions: Permission[]
|
||||
updateProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectMemberId,
|
||||
data
|
||||
}: {
|
||||
projectMemberId: string;
|
||||
data: {
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data);
|
||||
} catch (err) {
|
||||
@@ -76,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (_: any, { projectId, data }: {
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
addProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
}
|
||||
}) => {
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data));
|
||||
} catch (err) {
|
||||
@@ -91,25 +115,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
|
||||
addEnvironmentVariables: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string;
|
||||
data: { environments: string[]; key: string; value: string }[];
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
||||
return Boolean(
|
||||
await service.addEnvironmentVariables(projectId, data)
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
||||
updateEnvironmentVariable: async (
|
||||
_: any,
|
||||
{
|
||||
environmentVariableId,
|
||||
data
|
||||
}: {
|
||||
environmentVariableId: string;
|
||||
data: DeepPartial<EnvironmentVariable>;
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
||||
return await service.updateEnvironmentVariable(
|
||||
environmentVariableId,
|
||||
data
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
||||
removeEnvironmentVariable: async (
|
||||
_: any,
|
||||
{ environmentVariableId }: { environmentVariableId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||
} catch (err) {
|
||||
@@ -118,7 +168,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
||||
updateDeploymentToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
@@ -127,7 +181,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
|
||||
addProject: async (
|
||||
_: any,
|
||||
{
|
||||
organizationSlug,
|
||||
data
|
||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
} catch (err) {
|
||||
@@ -136,7 +197,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
|
||||
updateProject: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProject(projectId, data);
|
||||
} catch (err) {
|
||||
@@ -145,7 +209,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
||||
redeployToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||
} catch (err) {
|
||||
@@ -158,7 +226,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
try {
|
||||
return await service.deleteProject(projectId);
|
||||
} catch (err) {
|
||||
log(err); return false;
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -171,7 +240,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
||||
rollbackDeployment: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
deploymentId
|
||||
}: { deploymentId: string; projectId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.rollbackDeployment(projectId, deploymentId);
|
||||
} catch (err) {
|
||||
@@ -180,7 +255,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
|
||||
addDomain: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addDomain(projectId, data));
|
||||
} catch (err) {
|
||||
@@ -189,7 +267,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
|
||||
updateDomain: async (
|
||||
_: any,
|
||||
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateDomain(domainId, data);
|
||||
} catch (err) {
|
||||
@@ -198,7 +279,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
|
||||
authenticateGitHub: async (
|
||||
_: any,
|
||||
{ code }: { code: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.authenticateGitHub(code, context.user);
|
||||
} catch (err) {
|
||||
@@ -207,15 +292,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
}
|
||||
},
|
||||
|
||||
authenticateGit: async (_: any, { type, code }: { type: GitType, code: string }, context: any) => {
|
||||
try {
|
||||
return await service.authenticateGit(type, code, context.user);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||
|
||||
@@ -26,11 +26,6 @@ enum DomainStatus {
|
||||
Pending
|
||||
}
|
||||
|
||||
enum GitType {
|
||||
GitHub
|
||||
Gitea
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
name: String
|
||||
@@ -193,10 +188,19 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
||||
updateProjectMember(
|
||||
projectMemberId: String!
|
||||
data: UpdateProjectMemberInput
|
||||
): Boolean!
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
||||
addEnvironmentVariables(
|
||||
projectId: String!
|
||||
data: [AddEnvironmentVariableInput!]
|
||||
): Boolean!
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: String!
|
||||
data: UpdateEnvironmentVariableInput!
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
@@ -208,6 +212,5 @@ type Mutation {
|
||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||
authenticateGitHub(code: String!): AuthResult!
|
||||
authenticateGit(type: GitType!, code: String!): AuthResult!
|
||||
unauthenticateGitHub: Boolean!
|
||||
}
|
||||
|
||||
+184
-137
@@ -2,7 +2,6 @@ import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
import { Octokit, RequestError } from 'octokit';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app';
|
||||
|
||||
@@ -16,17 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { User } from './entity/User';
|
||||
import { Registry } from './registry';
|
||||
import { GitHubConfig, RegistryConfig } from './config';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types';
|
||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||
import { Role } from './entity/UserOrganization';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||
const GITEA_ACCESS_TOKEN_ENDPOINT = 'https://git.vdb.to/login/oauth/access_token';
|
||||
|
||||
interface Config {
|
||||
gitHubConfig: GitHubConfig
|
||||
registryConfig: RegistryConfig
|
||||
gitHubConfig: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
}
|
||||
|
||||
export class Service {
|
||||
@@ -74,7 +72,9 @@ export class Service {
|
||||
});
|
||||
|
||||
if (deployments.length) {
|
||||
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
|
||||
log(
|
||||
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||
);
|
||||
|
||||
// Fetch ApplicationDeploymentRecord for deployments
|
||||
const records = await this.registry.getDeploymentRecords(deployments);
|
||||
@@ -94,10 +94,12 @@ export class Service {
|
||||
/**
|
||||
* Update deployments with ApplicationDeploymentRecord data
|
||||
*/
|
||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
||||
async updateDeploymentsWithRecordData (
|
||||
records: AppDeploymentRecord[]
|
||||
): Promise<void> {
|
||||
// Get deployments for ApplicationDeploymentRecords
|
||||
const deployments = await this.db.getDeployments({
|
||||
where: records.map(record => ({
|
||||
where: records.map((record) => ({
|
||||
applicationRecordId: record.attributes.application
|
||||
})),
|
||||
order: {
|
||||
@@ -106,38 +108,46 @@ export class Service {
|
||||
});
|
||||
|
||||
// Get project IDs of deployments that are in production environment
|
||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
const productionDeploymentProjectIds = deployments.reduce(
|
||||
(acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
return acc;
|
||||
},
|
||||
new Set<string>()
|
||||
);
|
||||
|
||||
// Set old deployments isCurrent to false
|
||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
||||
await this.db.updateDeploymentsByProjectIds(
|
||||
Array.from(productionDeploymentProjectIds),
|
||||
{ isCurrent: false }
|
||||
);
|
||||
|
||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
return acc;
|
||||
}, {});
|
||||
const recordToDeploymentsMap = deployments.reduce(
|
||||
(acc: { [key: string]: Deployment }, deployment) => {
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// Update deployment data for ApplicationDeploymentRecords
|
||||
const deploymentUpdatePromises = records.map(async (record) => {
|
||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||
|
||||
await this.db.updateDeploymentById(
|
||||
deployment.id,
|
||||
{
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production
|
||||
}
|
||||
);
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production
|
||||
});
|
||||
|
||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
||||
log(
|
||||
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(deploymentUpdatePromises);
|
||||
@@ -183,7 +193,10 @@ export class Service {
|
||||
|
||||
async getOctokit (userId: string): Promise<Octokit> {
|
||||
const user = await this.db.getUser({ where: { id: userId } });
|
||||
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
|
||||
assert(
|
||||
user && user.gitHubToken,
|
||||
'User needs to be authenticated with GitHub token'
|
||||
);
|
||||
|
||||
return new Octokit({ auth: user.gitHubToken });
|
||||
}
|
||||
@@ -208,13 +221,19 @@ export class Service {
|
||||
return dbDeployments;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const dbEnvironmentVariables =
|
||||
await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||
return dbEnvironmentVariables;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const dbProjectMembers =
|
||||
await this.db.getProjectMembersByProjectId(projectId);
|
||||
return dbProjectMembers;
|
||||
}
|
||||
|
||||
@@ -223,20 +242,28 @@ export class Service {
|
||||
return dbProjects;
|
||||
}
|
||||
|
||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
||||
async getDomainsByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
||||
return dbDomains;
|
||||
}
|
||||
|
||||
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
||||
async updateProjectMember (
|
||||
projectMemberId: string,
|
||||
data: { permissions: Permission[] }
|
||||
): Promise<boolean> {
|
||||
return this.db.updateProjectMemberById(projectMemberId, data);
|
||||
}
|
||||
|
||||
async addProjectMember (projectId: string,
|
||||
async addProjectMember (
|
||||
projectId: string,
|
||||
data: {
|
||||
email: string,
|
||||
permissions: Permission[]
|
||||
}): Promise<ProjectMember> {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
}
|
||||
): Promise<ProjectMember> {
|
||||
// TODO: Send invitation
|
||||
let user = await this.db.getUser({
|
||||
where: {
|
||||
@@ -281,29 +308,41 @@ export class Service {
|
||||
}
|
||||
}
|
||||
|
||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
||||
return environmentVariable.environments.map((environment) => {
|
||||
return ({
|
||||
key: environmentVariable.key,
|
||||
value: environmentVariable.value,
|
||||
environment: environment as Environment,
|
||||
project: Object.assign(new Project(), {
|
||||
id: projectId
|
||||
})
|
||||
async addEnvironmentVariables (
|
||||
projectId: string,
|
||||
data: { environments: string[]; key: string; value: string }[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const formattedEnvironmentVariables = data
|
||||
.map((environmentVariable) => {
|
||||
return environmentVariable.environments.map((environment) => {
|
||||
return {
|
||||
key: environmentVariable.key,
|
||||
value: environmentVariable.value,
|
||||
environment: environment as Environment,
|
||||
project: Object.assign(new Project(), {
|
||||
id: projectId
|
||||
})
|
||||
};
|
||||
});
|
||||
});
|
||||
}).flat();
|
||||
})
|
||||
.flat();
|
||||
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
|
||||
formattedEnvironmentVariables
|
||||
);
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||
}
|
||||
|
||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
||||
async removeEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||
}
|
||||
|
||||
@@ -319,7 +358,10 @@ export class Service {
|
||||
throw new Error('Deployment does not exist');
|
||||
}
|
||||
|
||||
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch });
|
||||
const prodBranchDomains = await this.db.getDomainsByProjectId(
|
||||
oldDeployment.project.id,
|
||||
{ branch: oldDeployment.project.prodBranch }
|
||||
);
|
||||
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
@@ -344,7 +386,9 @@ export class Service {
|
||||
recordData: { repoUrl?: string } = {}
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`);
|
||||
log(
|
||||
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
|
||||
);
|
||||
const [owner, repo] = data.project.repository.split('/');
|
||||
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
@@ -364,18 +408,22 @@ export class Service {
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
if (!recordData.repoUrl) {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
recordData.repoUrl = repoDetails.html_url;
|
||||
}
|
||||
|
||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl: recordData.repoUrl
|
||||
});
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl: recordData.repoUrl
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
// TODO: Fix unique constraint error for domain
|
||||
@@ -441,7 +489,9 @@ export class Service {
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
const [owner, repo] = project.repository.split('/');
|
||||
|
||||
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
||||
const {
|
||||
data: [latestCommit]
|
||||
} = await octokit.rest.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: project.prodBranch,
|
||||
@@ -478,7 +528,10 @@ export class Service {
|
||||
owner,
|
||||
repo,
|
||||
config: {
|
||||
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href,
|
||||
url: new URL(
|
||||
'api/github/webhook',
|
||||
this.config.gitHubConfig.webhookUrl
|
||||
).href,
|
||||
content_type: 'json'
|
||||
},
|
||||
events: ['push']
|
||||
@@ -486,9 +539,13 @@ export class Service {
|
||||
} catch (err) {
|
||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||
if (
|
||||
!(err instanceof RequestError &&
|
||||
err.status === 422 &&
|
||||
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
|
||||
!(
|
||||
err instanceof RequestError &&
|
||||
err.status === 422 &&
|
||||
(err.response?.data as any).errors.some(
|
||||
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
@@ -500,7 +557,9 @@ export class Service {
|
||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||
const { repository, ref, head_commit: headCommit } = data;
|
||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||
const projects = await this.db.getProjects({ where: { repository: repository.full_name } });
|
||||
const projects = await this.db.getProjects({
|
||||
where: { repository: repository.full_name }
|
||||
});
|
||||
|
||||
if (!projects.length) {
|
||||
log(`No projects found for repository ${repository.full_name}`);
|
||||
@@ -512,23 +571,29 @@ export class Service {
|
||||
|
||||
for await (const project of projects) {
|
||||
const octokit = await this.getOctokit(project.ownerId);
|
||||
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
|
||||
const [domain] = await this.db.getDomainsByProjectId(project.id, {
|
||||
branch
|
||||
});
|
||||
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId,
|
||||
octokit,
|
||||
{
|
||||
project,
|
||||
branch,
|
||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message
|
||||
});
|
||||
await this.createDeployment(project.ownerId, octokit, {
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
||||
async updateProject (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
return this.db.updateProjectById(projectId, data);
|
||||
}
|
||||
|
||||
@@ -545,7 +610,9 @@ export class Service {
|
||||
});
|
||||
|
||||
if (domainsRedirectedFrom.length > 0) {
|
||||
throw new Error('Cannot delete domain since it has redirects from other domains');
|
||||
throw new Error(
|
||||
'Cannot delete domain since it has redirects from other domains'
|
||||
);
|
||||
}
|
||||
|
||||
return this.db.deleteDomainById(domainId);
|
||||
@@ -584,7 +651,10 @@ export class Service {
|
||||
return newDeployment;
|
||||
}
|
||||
|
||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
||||
async rollbackDeployment (
|
||||
projectId: string,
|
||||
deploymentId: string
|
||||
): Promise<boolean> {
|
||||
// TODO: Implement transactions
|
||||
const oldCurrentDeployment = await this.db.getDeployment({
|
||||
relations: {
|
||||
@@ -602,16 +672,25 @@ export class Service {
|
||||
throw new Error('Current deployment doesnot exist');
|
||||
}
|
||||
|
||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||
oldCurrentDeployment.id,
|
||||
{ isCurrent: false, domain: null }
|
||||
);
|
||||
|
||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||
deploymentId,
|
||||
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
|
||||
);
|
||||
|
||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||
}
|
||||
|
||||
async addDomain (projectId: string, data: { name: string }): Promise<{
|
||||
primaryDomain: Domain,
|
||||
redirectedDomain: Domain
|
||||
async addDomain (
|
||||
projectId: string,
|
||||
data: { name: string }
|
||||
): Promise<{
|
||||
primaryDomain: Domain;
|
||||
redirectedDomain: Domain;
|
||||
}> {
|
||||
const currentProject = await this.db.getProjectById(projectId);
|
||||
|
||||
@@ -636,12 +715,20 @@ export class Service {
|
||||
redirectTo: savedPrimaryDomain
|
||||
};
|
||||
|
||||
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
|
||||
const savedRedirectedDomain = await this.db.addDomain(
|
||||
redirectedDomainDetails
|
||||
);
|
||||
|
||||
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
|
||||
return {
|
||||
primaryDomain: savedPrimaryDomain,
|
||||
redirectedDomain: savedRedirectedDomain
|
||||
};
|
||||
}
|
||||
|
||||
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
||||
async updateDomain (
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
const domain = await this.db.getDomain({
|
||||
where: {
|
||||
id: domainId
|
||||
@@ -682,7 +769,9 @@ export class Service {
|
||||
}
|
||||
|
||||
if (redirectedDomain.redirectToId) {
|
||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
||||
throw new Error(
|
||||
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
|
||||
);
|
||||
}
|
||||
|
||||
newDomain.redirectTo = redirectedDomain;
|
||||
@@ -703,48 +792,6 @@ export class Service {
|
||||
return { token };
|
||||
}
|
||||
|
||||
async authenticateGit (type: GitType, code:string, user: User): Promise<{token: string}> {
|
||||
let token: string;
|
||||
|
||||
switch (type) {
|
||||
case GitType.GitHub:
|
||||
({ authentication: { token } } = await this.oauthApp.createToken({
|
||||
code
|
||||
}));
|
||||
|
||||
break;
|
||||
|
||||
case GitType.Gitea: {
|
||||
const response = await fetch(GITEA_ACCESS_TOKEN_ENDPOINT, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
// TODO: Fetch from config
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
// TODO: Get frontend app URL from config
|
||||
redirect_uri: 'http://localhost:3000/organization/projects/create'
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
assert(response.ok, `HTTP Error Response: ${response.status} ${response.statusText}`);
|
||||
const data: any = await response.json();
|
||||
({ access_token: token } = data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: throw new Error(`Type ${type} not handled for Git authentication`);
|
||||
}
|
||||
|
||||
assert(token, `Access token is not set for type ${type}`);
|
||||
await this.db.updateUser(user, { gitHubToken: token });
|
||||
|
||||
return { token };
|
||||
}
|
||||
|
||||
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||
return this.db.updateUser(user, data);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,5 @@ interface RegistryRecord {
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes
|
||||
}
|
||||
|
||||
export enum GitType {
|
||||
GitHub = 'GitHub',
|
||||
Gitea = 'Gitea',
|
||||
attributes: AppDeploymentRecordAttributes;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,15 @@ export const getEntities = async (filePath: string): Promise<any> => {
|
||||
return entities;
|
||||
};
|
||||
|
||||
export const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, entities: any, relations?: any | undefined): Promise<Entity[]> => {
|
||||
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
dataSource: DataSource,
|
||||
entities: any,
|
||||
relations?: any | undefined
|
||||
): Promise<Entity[]> => {
|
||||
const entityRepository = dataSource.getRepository(entityType);
|
||||
|
||||
const savedEntity:Entity[] = [];
|
||||
const savedEntity: Entity[] = [];
|
||||
|
||||
for (const entityData of entities) {
|
||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
||||
|
||||
@@ -18,4 +18,4 @@ const main = async () => {
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
||||
main().catch(err => log(err));
|
||||
main().catch((err) => log(err));
|
||||
|
||||
+20
-20
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":0,
|
||||
"domainIndex": 0,
|
||||
"createdByIndex": 0,
|
||||
"id":"ffhae3zq",
|
||||
"id": "ffhae3zq",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -18,9 +18,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":1,
|
||||
"domainIndex": 1,
|
||||
"createdByIndex": 0,
|
||||
"id":"vehagei8",
|
||||
"id": "vehagei8",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -35,9 +35,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex":2,
|
||||
"domainIndex": 2,
|
||||
"createdByIndex": 0,
|
||||
"id":"qmgekyte",
|
||||
"id": "qmgekyte",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -54,7 +54,7 @@
|
||||
"projectIndex": 0,
|
||||
"domainIndex": null,
|
||||
"createdByIndex": 0,
|
||||
"id":"f8wsyim6",
|
||||
"id": "f8wsyim6",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": false,
|
||||
@@ -69,9 +69,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":3,
|
||||
"domainIndex": 3,
|
||||
"createdByIndex": 1,
|
||||
"id":"eO8cckxk",
|
||||
"id": "eO8cckxk",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -86,9 +86,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":4,
|
||||
"domainIndex": 4,
|
||||
"createdByIndex": 1,
|
||||
"id":"yaq0t5yw",
|
||||
"id": "yaq0t5yw",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -103,9 +103,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex":5,
|
||||
"domainIndex": 5,
|
||||
"createdByIndex": 1,
|
||||
"id":"hwwr6sbx",
|
||||
"id": "hwwr6sbx",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -120,9 +120,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":9,
|
||||
"domainIndex": 9,
|
||||
"createdByIndex": 2,
|
||||
"id":"ndxje48a",
|
||||
"id": "ndxje48a",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":7,
|
||||
"domainIndex": 7,
|
||||
"createdByIndex": 2,
|
||||
"id":"gtgpgvei",
|
||||
"id": "gtgpgvei",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
@@ -154,9 +154,9 @@
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex":8,
|
||||
"domainIndex": 8,
|
||||
"createdByIndex": 2,
|
||||
"id":"b4bpthjr",
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
@@ -173,7 +173,7 @@
|
||||
"projectIndex": 3,
|
||||
"domainIndex": 6,
|
||||
"createdByIndex": 2,
|
||||
"id":"b4bpthjr",
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
|
||||
+9
-31
@@ -2,77 +2,55 @@
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View"
|
||||
],
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": [
|
||||
"View",
|
||||
"Edit"
|
||||
],
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
||||
import { Domain } from '../src/entity/Domain';
|
||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||
import { Deployment } from '../src/entity/Deployment';
|
||||
import { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils';
|
||||
import {
|
||||
checkFileExists,
|
||||
getConfig,
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
@@ -27,19 +32,34 @@ const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
||||
|
||||
const generateTestData = async (dataSource: DataSource) => {
|
||||
const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
|
||||
const userEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_DATA_PATH)
|
||||
);
|
||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||
|
||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities);
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
const savedOrgs = await loadAndSaveData(
|
||||
Organization,
|
||||
dataSource,
|
||||
orgEntities
|
||||
);
|
||||
|
||||
const projectRelations = {
|
||||
owner: savedUsers,
|
||||
organization: savedOrgs
|
||||
};
|
||||
|
||||
const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
|
||||
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
|
||||
const projectEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||
);
|
||||
const savedProjects = await loadAndSaveData(
|
||||
Project,
|
||||
dataSource,
|
||||
projectEntities,
|
||||
projectRelations
|
||||
);
|
||||
|
||||
const domainRepository = dataSource.getRepository(Domain);
|
||||
|
||||
@@ -47,16 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
|
||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations);
|
||||
const primaryDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||
);
|
||||
const savedPrimaryDomains = await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
primaryDomainsEntities,
|
||||
domainPrimaryRelations
|
||||
);
|
||||
|
||||
const domainRedirectedRelations = {
|
||||
project: savedProjects,
|
||||
redirectTo: savedPrimaryDomains
|
||||
};
|
||||
|
||||
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
|
||||
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
|
||||
const redirectDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
redirectDomainsEntities,
|
||||
domainRedirectedRelations
|
||||
);
|
||||
|
||||
const savedDomains = await domainRepository.find();
|
||||
|
||||
@@ -65,16 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
organization: savedOrgs
|
||||
};
|
||||
|
||||
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
|
||||
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
|
||||
const userOrganizationsEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
UserOrganization,
|
||||
dataSource,
|
||||
userOrganizationsEntities,
|
||||
userOrganizationRelations
|
||||
);
|
||||
|
||||
const projectMemberRelations = {
|
||||
member: savedUsers,
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
|
||||
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
|
||||
const projectMembersEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
ProjectMember,
|
||||
dataSource,
|
||||
projectMembersEntities,
|
||||
projectMemberRelations
|
||||
);
|
||||
|
||||
const deploymentRelations = {
|
||||
project: savedProjects,
|
||||
@@ -82,15 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
createdBy: savedUsers
|
||||
};
|
||||
|
||||
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
|
||||
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
|
||||
const deploymentsEntities = await getEntities(
|
||||
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
Deployment,
|
||||
dataSource,
|
||||
deploymentsEntities,
|
||||
deploymentRelations
|
||||
);
|
||||
|
||||
const environmentVariableRelations = {
|
||||
project: savedProjects
|
||||
};
|
||||
|
||||
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
|
||||
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
|
||||
const environmentVariablesEntities = await getEntities(
|
||||
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||
);
|
||||
await loadAndSaveData(
|
||||
EnvironmentVariable,
|
||||
dataSource,
|
||||
environmentVariablesEntities,
|
||||
environmentVariableRelations
|
||||
);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
|
||||
@@ -21,12 +21,20 @@ async function main () {
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
registryConfig.fee
|
||||
);
|
||||
log(`Bond ${bondId} set for authority ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ const log = debug('snowball:publish-deploy-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 registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
registryConfig.chainId
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_GITEA_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
@@ -16,5 +16,10 @@
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
]
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
@@ -3,8 +3,17 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -16,13 +25,15 @@
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"date-fns": "^3.0.1",
|
||||
"downshift": "^8.2.3",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.3.2",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"gql-client": "^1.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
"octokit": "^3.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^4.8.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -34,6 +45,7 @@
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
@@ -78,6 +90,6 @@
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.3.6"
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,29 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="snowball tools dashboard"
|
||||
/>
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#2d89ef">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Snowball</title>
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "Snowball Tools Dashboard",
|
||||
"short_name": "snowball tools",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Progressive Web App (PWA)",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Kotlin",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "React Native",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "Swift",
|
||||
"icon": "^"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "Web app",
|
||||
"icon": "^"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
export default [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: '^',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Kotlin',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Swift',
|
||||
icon: '^',
|
||||
repoFullName: '',
|
||||
},
|
||||
];
|
||||
@@ -126,7 +126,8 @@ const DatePicker = ({
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
</PopoverHandler>
|
||||
<PopoverContent>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<PopoverContent placeholder={''}>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -145,19 +146,23 @@ const DatePicker = ({
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<div className="flex justify-end">
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
placeholder={''}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const HorizontalLine = () => {
|
||||
return <hr className="h-px bg-gray-300 border-0" />;
|
||||
return <hr className="h-px bg-gray-100 border-0" />;
|
||||
};
|
||||
|
||||
export default HorizontalLine;
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import React, { forwardRef, RefAttributes } from 'react';
|
||||
|
||||
import { Input, InputProps } from '@material-tailwind/react';
|
||||
import { SearchIcon } from './shared/CustomIcon';
|
||||
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 gap-2">
|
||||
<div className="relative flex w-full">
|
||||
<Input
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
type="search"
|
||||
placeholder={placeholder}
|
||||
containerProps={{
|
||||
className: 'min-w-[288px]',
|
||||
}}
|
||||
className="!border-t-blue-gray-300 pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
|
||||
labelProps={{
|
||||
className: 'before:content-none after:content-none',
|
||||
}}
|
||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
crossOrigin={undefined}
|
||||
appearance={'borderless'}
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
/>
|
||||
<div className="!absolute left-3 top-[13px]">
|
||||
<i>^</i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,20 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization } from 'gql-client';
|
||||
|
||||
import { Typography, Option } from '@material-tailwind/react';
|
||||
import { Option } from '@material-tailwind/react';
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from '../context/GQLClientContext';
|
||||
import AsyncSelect from './shared/AsyncSelect';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
FolderIcon,
|
||||
GlobeIcon,
|
||||
LifeBuoyIcon,
|
||||
QuestionMarkRoundIcon,
|
||||
SettingsSlidersIcon,
|
||||
} from './shared/CustomIcon';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const Sidebar = () => {
|
||||
const { orgSlug } = useParams();
|
||||
@@ -33,61 +42,95 @@ const Sidebar = () => {
|
||||
}, [disconnect, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="flex flex-col h-full p-4 pt-10">
|
||||
<div className="grow">
|
||||
<div>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<h3 className="text-black text-2xl">Snowball</h3>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<div className="flex items-center space-x-3 mb-10 ml-2">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Snowball Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<span className="text-2xl font-bold text-snowball-900">
|
||||
Snowball
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<AsyncSelect
|
||||
className="bg-white py-2"
|
||||
containerProps={{ className: 'h-14 border-none' }}
|
||||
labelProps={{ className: 'before:border-none after:border-none' }}
|
||||
className="bg-white rounded-lg shadow border-none"
|
||||
value={selectedOrgSlug}
|
||||
onChange={(value) => {
|
||||
setSelectedOrgSlug(value!);
|
||||
navigate(`/${value}`);
|
||||
}}
|
||||
selected={(_, index) => (
|
||||
<div className="flex gap-2">
|
||||
<div>^</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Application Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<span>{organizations[index!]?.name}</span>
|
||||
<Typography>Organization</Typography>
|
||||
<div className="text-sm font-semibold">
|
||||
{organizations[index!]?.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Organization</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
||||
>
|
||||
{/* TODO: Show label organization and manage in option */}
|
||||
{organizations.map((org) => (
|
||||
<Option key={org.id} value={org.slug}>
|
||||
^ {org.name}
|
||||
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Application Logo"
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{org.name}</div>
|
||||
<div className="text-xs text-gray-500">Organization</div>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Projects</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
<div>
|
||||
<NavLink
|
||||
to={`/${orgSlug}/settings`}
|
||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
||||
>
|
||||
<Typography>Settings</Typography>
|
||||
</NavLink>
|
||||
</div>
|
||||
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
|
||||
<Tabs.List>
|
||||
{[
|
||||
{ title: 'Projects', icon: <FolderIcon /> },
|
||||
{ title: 'Settings', icon: <SettingsSlidersIcon /> },
|
||||
].map(({ title, icon }, index) => (
|
||||
<NavLink to={`/${orgSlug}/${title}`} key={index}>
|
||||
<Tabs.Trigger icon={icon} value={title}>
|
||||
{title}
|
||||
</Tabs.Trigger>
|
||||
</NavLink>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="grow flex flex-col justify-end">
|
||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||
Log Out
|
||||
</a>
|
||||
<a className="cursor-pointer">Documentation</a>
|
||||
<a className="cursor-pointer">Support</a>
|
||||
<div className="grow flex flex-col justify-end mb-8">
|
||||
<Tabs defaultValue="Projects" orientation="vertical">
|
||||
{/* TODO: use proper link buttons */}
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger icon={<GlobeIcon />} value="">
|
||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||
Log Out
|
||||
</a>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger icon={<QuestionMarkRoundIcon />} value="">
|
||||
<a className="cursor-pointer">Documentation</a>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger icon={<LifeBuoyIcon />} value="">
|
||||
<a className="cursor-pointer">Support</a>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,11 +21,15 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
||||
<div className="flex gap-2 p-2 items-center">
|
||||
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
src={project.icon || '/gray.png'}
|
||||
placeholder={''}
|
||||
/>
|
||||
<div className="grow">
|
||||
<Link to={`projects/${project.id}`}>
|
||||
<Typography>{project.name}</Typography>
|
||||
<Typography color="gray" variant="small">
|
||||
<Typography placeholder={''}>{project.name}</Typography>
|
||||
<Typography color="gray" variant="small" placeholder={''}>
|
||||
{project.deployments[0]?.domain?.name ??
|
||||
'No Production Deployment'}
|
||||
</Typography>
|
||||
@@ -35,25 +39,27 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
||||
<MenuHandler>
|
||||
<button>...</button>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem>^ Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500">^ Delete project</MenuItem>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem placeholder={''}>^ Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500" placeholder={''}>
|
||||
^ Delete project
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
||||
{project.deployments.length > 0 ? (
|
||||
<>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
^ {project.deployments[0].commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{relativeTimeMs(project.deployments[0].createdAt)} on ^
|
||||
{project.deployments[0].branch}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
No Production deployment
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
||||
(!inputValue || !isOpen) && 'hidden'
|
||||
}`}
|
||||
placeholder={''}
|
||||
>
|
||||
<List {...getMenuProps()}>
|
||||
{items.length ? (
|
||||
<>
|
||||
<div className="p-3">
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
Suggestions
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
<ListItem
|
||||
selected={highlightedIndex === index || selectedItem === item}
|
||||
key={item.id}
|
||||
placeholder={''}
|
||||
{...getItemProps({ item, index })}
|
||||
>
|
||||
<ListItemPrefix>
|
||||
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
|
||||
<ListItemPrefix placeholder={''}>
|
||||
<Avatar
|
||||
src={item.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
/>
|
||||
</ListItemPrefix>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray">
|
||||
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
className="font-normal"
|
||||
placeholder={''}
|
||||
>
|
||||
{item.organization.name}
|
||||
</Typography>
|
||||
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
||||
</>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<Typography>^ No projects matching this name</Typography>
|
||||
<Typography placeholder={''}>
|
||||
^ No projects matching this name
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -1,73 +1,75 @@
|
||||
import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
import { GitType } from 'gql-client';
|
||||
|
||||
import { Button } from '@material-tailwind/react';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
GitIcon,
|
||||
EllipsesIcon,
|
||||
SnowballIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
|
||||
const REDIRECT_URI = `${window.location.origin}/organization/projects/create`;
|
||||
const GITEA_OAUTH_URL = `https://git.vdb.to/login/oauth/authorize?client_id=${process.env.REACT_APP_GITEA_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;
|
||||
|
||||
interface ConnectAccountInterface {
|
||||
onAuth: (token: string) => void;
|
||||
}
|
||||
|
||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||
onAuth: onToken,
|
||||
}: ConnectAccountInterface) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const handleCode = async (type: GitType, code: string) => {
|
||||
const handleCode = async (code: string) => {
|
||||
// Pass code to backend and get access token
|
||||
const {
|
||||
authenticateGit: { token },
|
||||
} = await client.authenticateGit(type, code);
|
||||
|
||||
// TODO: Handle token according to Git type
|
||||
authenticateGitHub: { token },
|
||||
} = await client.authenticateGitHub(code);
|
||||
onToken(token);
|
||||
};
|
||||
|
||||
// TODO: Use correct height
|
||||
return (
|
||||
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div>^</div>
|
||||
<div>
|
||||
<p>Connect to your git account</p>
|
||||
<p>
|
||||
Once connected, you can import a repository from your
|
||||
<br />
|
||||
account or start with one of our templates.
|
||||
</p>
|
||||
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 inline-flex">
|
||||
<GitIcon />
|
||||
</div>
|
||||
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||
<div className="w-16 h-16 bg-blue-400 rounded-2xl shadow-inner border border-sky-950 border-opacity-10 justify-center items-center gap-2.5 flex">
|
||||
<SnowballIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex">
|
||||
<div>
|
||||
<div className="text-center text-slate-900 text-xl font-medium leading-7">
|
||||
Connect to your Git account
|
||||
</div>
|
||||
<div className="text-center text-slate-600 text-base font-normal leading-normal">
|
||||
Once connected, you can import a repository from your account or start
|
||||
with one of our templates.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-3">
|
||||
<OauthPopup
|
||||
url={GITHUB_OAUTH_URL}
|
||||
onCode={(code) => handleCode(GitType.GitHub, code)}
|
||||
onCode={handleCode}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
||||
</OauthPopup>
|
||||
<OauthPopup
|
||||
url={GITEA_OAUTH_URL}
|
||||
onCode={(code) => handleCode(GitType.Gitea, code)}
|
||||
onClose={() => {}}
|
||||
title="Snowball"
|
||||
width={1000}
|
||||
height={1000}
|
||||
>
|
||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
||||
<Button>Connect to GitHub</Button>
|
||||
</OauthPopup>
|
||||
<Button>Connect to Gitea</Button>
|
||||
</div>
|
||||
<ConnectAccountTabPanel />
|
||||
|
||||
{/* TODO: Add ConnectAccountTabPanel */}
|
||||
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||
<ConnectAccountTabPanel />
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const ConnectAccountTabPanel = () => {
|
||||
const ConnectAccountTabPanel: React.FC = () => {
|
||||
return (
|
||||
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
|
||||
<TabsHeader className="grid grid-cols-2">
|
||||
<Tab className="row-span-1" value="import">
|
||||
Import a repository
|
||||
</Tab>
|
||||
<Tab className="row-span-2" value="template">
|
||||
Start with a template
|
||||
</Tab>
|
||||
</TabsHeader>
|
||||
{/* <TabsBody> */}
|
||||
{/* TODO: Add content */}
|
||||
{/* </TabsBody> */}
|
||||
<Tabs
|
||||
defaultValue="Connect Accounts Tab Panel"
|
||||
orientation="horizontal"
|
||||
className="mt-10"
|
||||
>
|
||||
<Tabs.List>
|
||||
{[
|
||||
{ title: 'Import a repository' },
|
||||
{ title: 'Start with a template' },
|
||||
].map(({ title }, index) => (
|
||||
<Tabs.Trigger value={title} key={index}>
|
||||
{title}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||
|
||||
const INTERVAL_DURATION = 5000;
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId');
|
||||
@@ -25,7 +25,7 @@ const Deploy = () => {
|
||||
useEffect(() => {
|
||||
const timerID = setTimeout(() => {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}, INTERVAL_DURATION);
|
||||
}, TIMEOUT_DURATION);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
@@ -43,7 +43,12 @@ const Deploy = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Cancel
|
||||
</Button>
|
||||
</div>
|
||||
@@ -55,7 +60,7 @@ const Deploy = () => {
|
||||
handleConfirm={handleCancel}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
This will halt the deployment and you will have to start the process
|
||||
from scratch.
|
||||
</Typography>
|
||||
|
||||
@@ -62,7 +62,12 @@ const DeployStep = ({
|
||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
||||
{processLogs.map((log, key) => {
|
||||
return (
|
||||
<Typography variant="small" color="gray" key={key}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
{log}
|
||||
</Typography>
|
||||
);
|
||||
@@ -75,6 +80,7 @@ const DeployStep = ({
|
||||
toast.success('Logs copied');
|
||||
}}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Copy log
|
||||
</Button>
|
||||
|
||||
@@ -66,7 +66,9 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm">{'>'}</IconButton>
|
||||
<IconButton size="sm" placeholder={''}>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -136,11 +136,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Typography>No repository found</Typography>
|
||||
<Typography placeholder={''}>No repository found</Typography>
|
||||
<Button
|
||||
className="rounded-full mt-5"
|
||||
size="sm"
|
||||
onClick={handleResetFilters}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Reset filters
|
||||
</Button>
|
||||
|
||||
@@ -18,11 +18,15 @@ interface TemplateCardProps {
|
||||
const CardDetails = ({ template }: { template: TemplateDetails }) => {
|
||||
return (
|
||||
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between">
|
||||
<Typography className="grow">
|
||||
<Typography className="grow" placeholder={''}>
|
||||
{template.icon} {template.name}
|
||||
</Typography>
|
||||
<div>
|
||||
<IconButton size="sm" className="rounded-full hidden group-hover:block">
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full hidden group-hover:block"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -13,20 +13,29 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
return (
|
||||
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
|
||||
<div className="w-8">
|
||||
<Avatar src={activity.author?.avatar_url} variant="rounded" size="sm" />
|
||||
<Avatar
|
||||
src={activity.author?.avatar_url}
|
||||
variant="rounded"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
/>
|
||||
</div>
|
||||
<div className="grow">
|
||||
<Typography>{activity.commit.author?.name}</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography placeholder={''}>{activity.commit.author?.name}</Typography>
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{relativeTimeISO(activity.commit.author!.date!)} ^{' '}
|
||||
{activity.branch.name}
|
||||
</Typography>
|
||||
<Typography variant="small" color="gray">
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
{activity.commit.message}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="mr-2 self-center hidden group-hover:block">
|
||||
<IconButton size="sm" className="rounded-full bg-gray-600">
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full bg-gray-600"
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
+5
-4
@@ -17,9 +17,9 @@ interface AssignDomainProps {
|
||||
|
||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader>Assign Domain</DialogHeader>
|
||||
<DialogBody>
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
|
||||
<DialogBody placeholder={''}>
|
||||
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 */}
|
||||
@@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
theme={atomOneLight}
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
onClick={handleOpen}
|
||||
placeholder={''}
|
||||
>
|
||||
<span>Okay</span>
|
||||
</Button>
|
||||
|
||||
+31
-16
@@ -93,10 +93,12 @@ const DeploymentDetailsCard = ({
|
||||
<div className="col-span-3">
|
||||
<div className="flex">
|
||||
{deployment.url && (
|
||||
<Typography className=" basis-3/4">{deployment.url}</Typography>
|
||||
<Typography className="basis-3/4" placeholder={''}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Typography color="gray">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
@@ -111,14 +113,16 @@ const DeploymentDetailsCard = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Typography color="gray">^ {deployment.branch}</Typography>
|
||||
<Typography color="gray">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.branch}
|
||||
</Typography>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Typography color="gray" className="grow">
|
||||
<Typography color="gray" className="grow" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
<Tooltip content={deployment.createdBy.name}>
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
@@ -128,18 +132,22 @@ const DeploymentDetailsCard = ({
|
||||
<MenuHandler>
|
||||
<button className="self-start">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuList placeholder={''}>
|
||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
||||
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
|
||||
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
|
||||
^ Visit
|
||||
</MenuItem>
|
||||
</a>
|
||||
<MenuItem
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Assign domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Change to production
|
||||
</MenuItem>
|
||||
@@ -152,6 +160,7 @@ const DeploymentDetailsCard = ({
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Redeploy to production
|
||||
</MenuItem>
|
||||
@@ -162,6 +171,7 @@ const DeploymentDetailsCard = ({
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Rollback to this version
|
||||
</MenuItem>
|
||||
@@ -180,17 +190,22 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will be changed to production.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
The new deployment will be associated with these domains:
|
||||
</Typography>
|
||||
{prodBranchDomains.length > 0 &&
|
||||
prodBranchDomains.map((value) => {
|
||||
return (
|
||||
<Typography variant="small" color="blue" key={value.id}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color="blue"
|
||||
key={value.id}
|
||||
placeholder={''}
|
||||
>
|
||||
^ {value.name}
|
||||
</Typography>
|
||||
);
|
||||
@@ -209,16 +224,16 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, new deployment will be created with the same
|
||||
source code as current deployment.
|
||||
</Typography>
|
||||
<DeploymentDialogBodyCard deployment={deployment} />
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
{deployment.domain?.name && (
|
||||
<Typography variant="small" color="blue">
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
{deployment.domain?.name}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -237,7 +252,7 @@ const DeploymentDetailsCard = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Upon confirmation, this deployment will replace your current
|
||||
deployment
|
||||
</Typography>
|
||||
@@ -255,10 +270,10 @@ const DeploymentDetailsCard = ({
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
These domains will point to your new deployment:
|
||||
</Typography>
|
||||
<Typography variant="small" color="blue">
|
||||
<Typography variant="small" color="blue" placeholder={''}>
|
||||
^ {currentDeployment.domain?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -20,7 +20,7 @@ const DeploymentDialogBodyCard = ({
|
||||
deployment,
|
||||
}: DeploymentDialogBodyCardProps) => {
|
||||
return (
|
||||
<Card className="p-2 shadow-none">
|
||||
<Card className="p-2 shadow-none" placeholder={''}>
|
||||
{chip && (
|
||||
<Chip
|
||||
className={`w-fit normal-case font-normal`}
|
||||
@@ -30,16 +30,16 @@ const DeploymentDialogBodyCard = ({
|
||||
/>
|
||||
)}
|
||||
{deployment.url && (
|
||||
<Typography variant="small" className="text-black">
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
{deployment.url}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {deployment.branch} ^{' '}
|
||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
{deployment.commitMessage}
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
{formatAddress(deployment.createdBy.name ?? '')}
|
||||
</Typography>
|
||||
|
||||
@@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</IconButton>
|
||||
|
||||
+7
-2
@@ -21,7 +21,9 @@ const AddEnvironmentVariableRow = ({
|
||||
return (
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.key`, {
|
||||
@@ -30,7 +32,9 @@ const AddEnvironmentVariableRow = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
@@ -43,6 +47,7 @@ const AddEnvironmentVariableRow = ({
|
||||
size="sm"
|
||||
onClick={() => onDelete()}
|
||||
disabled={isDeleteDisabled}
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
|
||||
@@ -61,23 +61,26 @@ const AddMemberDialog = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Add member</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small">Email address</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Email address
|
||||
</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
crossOrigin={undefined}
|
||||
@@ -85,8 +88,10 @@ const AddMemberDialog = ({
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small">Permissions</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Permissions
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
@@ -102,8 +107,13 @@ const AddMemberDialog = ({
|
||||
color="blue"
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -111,6 +121,7 @@ const AddMemberDialog = ({
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Send invite
|
||||
</Button>
|
||||
|
||||
@@ -53,20 +53,21 @@ const DeleteProjectDialog = ({
|
||||
}, [client, project, handleOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Delete project?</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2">
|
||||
<Typography variant="paragraph">
|
||||
<DialogBody className="flex flex-col gap-2" placeholder={''}>
|
||||
<Typography variant="paragraph" placeholder={''}>
|
||||
Deleting your project is irreversible. Enter your project’s
|
||||
name
|
||||
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
||||
@@ -80,12 +81,17 @@ const DeleteProjectDialog = ({
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" color="red">
|
||||
<Typography variant="small" color="red" placeholder={''}>
|
||||
^ Deleting your project is irreversible.
|
||||
</Typography>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -93,6 +99,7 @@ const DeleteProjectDialog = ({
|
||||
color="red"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Yes, Delete project
|
||||
</Button>
|
||||
|
||||
+4
-5
@@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
|
||||
|
||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
interface DisplayEnvironmentVariablesProps {
|
||||
environment: Environment;
|
||||
@@ -30,11 +29,11 @@ const DisplayEnvironmentVariables = ({
|
||||
</div>
|
||||
<Collapse open={openCollapse}>
|
||||
{variables.length === 0 ? (
|
||||
<Card className="bg-gray-300 flex items-center p-4">
|
||||
<Typography variant="small" className="text-black">
|
||||
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
No environment variables added yet.
|
||||
</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once you add them, they’ll show up here.
|
||||
</Typography>
|
||||
</Card>
|
||||
|
||||
@@ -68,7 +68,7 @@ const DomainCard = ({
|
||||
<>
|
||||
<div className="flex justify-between py-3">
|
||||
<div className="flex justify-start gap-1">
|
||||
<Typography variant="h6">
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
<i>^</i> {domain.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
@@ -97,18 +97,20 @@ const DomainCard = ({
|
||||
<MenuHandler>
|
||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
setEditDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Edit domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Delete domain
|
||||
</MenuItem>
|
||||
@@ -127,7 +129,7 @@ const DomainCard = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once deleted, the project{' '}
|
||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||
{project.name}
|
||||
@@ -140,15 +142,21 @@ const DomainCard = ({
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
||||
<Typography variant="small">Production</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Production
|
||||
</Typography>
|
||||
{domain.status === DomainStatus.Pending && (
|
||||
<Card className="bg-gray-200 p-4 text-sm">
|
||||
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
|
||||
{refreshStatus === RefreshStatus.IDLE ? (
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
^ Add these records to your domain and refresh to check
|
||||
</Typography>
|
||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||
<Typography variant="small" className="text-blue-500">
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-blue-500"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Checking records for {domain.name}
|
||||
</Typography>
|
||||
) : (
|
||||
|
||||
@@ -122,27 +122,32 @@ const EditDomainDialog = ({
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Edit domain</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input crossOrigin={undefined} {...register('name')} />
|
||||
<Typography variant="small">Redirect to</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Redirect to
|
||||
</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown}>
|
||||
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
@@ -154,14 +159,16 @@ const EditDomainDialog = ({
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Typography variant="small">Git branch</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Git branch
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register('branch', {
|
||||
@@ -174,13 +181,22 @@ const EditDomainDialog = ({
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography variant="small" className="text-red-500">
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-red-500"
|
||||
placeholder={''}
|
||||
>
|
||||
We couldn't find this branch in the connected Git repository.
|
||||
</Typography>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -188,6 +204,7 @@ const EditDomainDialog = ({
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
placeholder={''}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
|
||||
+11
-3
@@ -84,7 +84,9 @@ const EditEnvironmentVariableRow = ({
|
||||
<>
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
@@ -92,7 +94,9 @@ const EditEnvironmentVariableRow = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
@@ -114,6 +118,7 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
{'S'}
|
||||
</IconButton>
|
||||
@@ -125,6 +130,7 @@ const EditEnvironmentVariableRow = ({
|
||||
reset();
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'C'}
|
||||
</IconButton>
|
||||
@@ -138,6 +144,7 @@ const EditEnvironmentVariableRow = ({
|
||||
onClick={() => {
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'E'}
|
||||
</IconButton>
|
||||
@@ -146,6 +153,7 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'D'}
|
||||
</IconButton>
|
||||
@@ -162,7 +170,7 @@ const EditEnvironmentVariableRow = ({
|
||||
handleConfirm={removeEnvironmentVariableHandler}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable
|
||||
<span className="bg-blue-100">{variable.key}</span>?
|
||||
</Typography>
|
||||
|
||||
@@ -104,6 +104,7 @@ const MemberCard = ({
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
placeholder={''}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
@@ -132,6 +133,7 @@ const MemberCard = ({
|
||||
onClick={() => {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
D
|
||||
</IconButton>
|
||||
@@ -152,7 +154,7 @@ const MemberCard = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once removed, {formatAddress(member.name ?? '')} (
|
||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
||||
this project.
|
||||
|
||||
@@ -17,14 +17,19 @@ const RepoConnectedSection = ({
|
||||
<div className="flex gap-4">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<Typography variant="small">{linkedRepo.full_name}</Typography>
|
||||
<Typography variant="small">Connected just now</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
{linkedRepo.full_name}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Connected just now
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Disconnect
|
||||
</Button>
|
||||
@@ -39,7 +44,7 @@ const RepoConnectedSection = ({
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Any data tied to your Git project may become misconfigured. Are you
|
||||
sure you want to continue?
|
||||
</Typography>
|
||||
|
||||
@@ -54,14 +54,18 @@ const SetupDomain = () => {
|
||||
className="flex flex-col gap-6 w-full"
|
||||
>
|
||||
<div>
|
||||
<Typography variant="h5">Setup domain name</Typography>
|
||||
<Typography variant="small">
|
||||
<Typography variant="h5" placeholder={''}>
|
||||
Setup domain name
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Add your domain and setup redirects
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="w-auto">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input
|
||||
type="text"
|
||||
variant="outlined"
|
||||
@@ -76,7 +80,7 @@ const SetupDomain = () => {
|
||||
|
||||
{isValid && (
|
||||
<div>
|
||||
<Typography>Primary domain</Typography>
|
||||
<Typography placeholder={''}>Primary domain</Typography>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Radio
|
||||
label={domainStr}
|
||||
@@ -108,6 +112,7 @@ const SetupDomain = () => {
|
||||
className="w-fit"
|
||||
color={isValid ? 'blue' : 'gray'}
|
||||
type="submit"
|
||||
placeholder={''}
|
||||
>
|
||||
<i>^</i> Next
|
||||
</Button>
|
||||
|
||||
@@ -23,6 +23,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
navigator.clipboard.writeText(webhookUrl);
|
||||
toast.success('Copied to clipboard');
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
C
|
||||
</Button>
|
||||
@@ -32,6 +33,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
@@ -48,7 +50,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
<Typography variant="small">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Are you sure you want to delete the variable{' '}
|
||||
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
|
||||
</Typography>
|
||||
|
||||
@@ -9,7 +9,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
|
||||
|
||||
useEffect(() => setKey((preVal) => preVal + 1), [props]);
|
||||
|
||||
return <Select key={key} ref={ref} {...props} />;
|
||||
return <Select key={key} ref={ref} {...props} placeholder={''} />;
|
||||
});
|
||||
|
||||
AsyncSelect.displayName = 'AsyncSelect';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
'w-full',
|
||||
'rounded-[inherit]',
|
||||
'object-cover',
|
||||
'object-center',
|
||||
],
|
||||
fallback: [
|
||||
'grid',
|
||||
'select-none',
|
||||
'place-content-center',
|
||||
'h-full',
|
||||
'w-full',
|
||||
'rounded-[inherit]',
|
||||
'font-medium',
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
gray: {
|
||||
fallback: ['text-elements-highEm', 'bg-base-bg-emphasized'],
|
||||
},
|
||||
orange: {
|
||||
fallback: ['text-elements-warning', 'bg-base-bg-emphasized-warning'],
|
||||
},
|
||||
blue: {
|
||||
fallback: ['text-elements-info', 'bg-base-bg-emphasized-info'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
18: {
|
||||
base: ['rounded-md', 'h-[18px]', 'w-[18px]', 'text-[0.625rem]'],
|
||||
},
|
||||
20: {
|
||||
base: ['rounded-md', 'h-5', 'w-5', 'text-[0.625rem]'],
|
||||
},
|
||||
24: {
|
||||
base: ['rounded-md', 'h-6', 'w-6', 'text-[0.625rem]'],
|
||||
},
|
||||
28: {
|
||||
base: ['rounded-lg', 'h-[28px]', 'w-[28px]', 'text-[0.625rem]'],
|
||||
},
|
||||
32: {
|
||||
base: ['rounded-lg', 'h-8', 'w-8', 'text-xs'],
|
||||
},
|
||||
36: {
|
||||
base: ['rounded-xl', 'h-[36px]', 'w-[36px]', 'text-xs'],
|
||||
},
|
||||
40: {
|
||||
base: ['rounded-xl', 'h-10', 'w-10', 'text-sm'],
|
||||
},
|
||||
44: {
|
||||
base: ['rounded-xl', 'h-[44px]', 'w-[44px]', 'text-sm'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 24,
|
||||
type: 'gray',
|
||||
},
|
||||
},
|
||||
{ responsiveVariants: true },
|
||||
);
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarTheme>;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { type ComponentPropsWithoutRef, type ComponentProps } from 'react';
|
||||
import { avatarTheme, type AvatarVariants } from './Avatar.theme';
|
||||
import * as PrimitiveAvatar from '@radix-ui/react-avatar';
|
||||
|
||||
export type AvatarProps = ComponentPropsWithoutRef<'div'> & {
|
||||
imageSrc?: string | null;
|
||||
initials?: string;
|
||||
imageProps?: ComponentProps<typeof PrimitiveAvatar.Image>;
|
||||
fallbackProps?: ComponentProps<typeof PrimitiveAvatar.Fallback>;
|
||||
} & AvatarVariants;
|
||||
|
||||
export const Avatar = ({
|
||||
className,
|
||||
size,
|
||||
type,
|
||||
imageSrc,
|
||||
imageProps,
|
||||
fallbackProps,
|
||||
initials,
|
||||
}: AvatarProps) => {
|
||||
const { base, image, fallback } = avatarTheme({ size, type });
|
||||
|
||||
return (
|
||||
<PrimitiveAvatar.Root className={base({ className })}>
|
||||
{imageSrc && (
|
||||
<PrimitiveAvatar.Image
|
||||
{...imageProps}
|
||||
className={image({ className: imageProps?.className })}
|
||||
src={imageSrc}
|
||||
/>
|
||||
)}
|
||||
<PrimitiveAvatar.Fallback asChild {...fallbackProps}>
|
||||
<div className={fallback({ className: fallbackProps?.className })}>
|
||||
{initials}
|
||||
</div>
|
||||
</PrimitiveAvatar.Fallback>
|
||||
</PrimitiveAvatar.Root>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Avatar';
|
||||
export * from './Avatar.theme';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const badgeTheme = tv({
|
||||
slots: {
|
||||
wrapper: ['rounded-full', 'grid', 'place-content-center'],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
primary: {
|
||||
wrapper: ['bg-controls-primary', 'text-elements-on-primary'],
|
||||
},
|
||||
secondary: {
|
||||
wrapper: ['bg-controls-secondary', 'text-elements-on-secondary'],
|
||||
},
|
||||
tertiary: {
|
||||
wrapper: [
|
||||
'bg-controls-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'text-elements-high-em',
|
||||
'shadow-button',
|
||||
],
|
||||
},
|
||||
inset: {
|
||||
wrapper: ['bg-controls-inset', 'text-elements-high-em'],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['h-5', 'w-5', 'text-xs'],
|
||||
},
|
||||
xs: {
|
||||
wrapper: ['h-4', 'w-4', 'text-2xs', 'font-medium'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'sm',
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeTheme = VariantProps<typeof badgeTheme>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { BadgeTheme, badgeTheme } from './Badge.theme';
|
||||
|
||||
export interface BadgeProps
|
||||
extends ComponentPropsWithoutRef<'div'>,
|
||||
BadgeTheme {}
|
||||
|
||||
/**
|
||||
* A badge is a small status descriptor for UI elements.
|
||||
* It can be used to indicate a status, a count, or a category.
|
||||
* It is typically used in lists, tables, or navigation elements.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Badge variant="primary" size="sm">1</Badge
|
||||
* ```
|
||||
*/
|
||||
export const Badge = ({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: BadgeProps) => {
|
||||
const { wrapper } = badgeTheme();
|
||||
|
||||
return (
|
||||
<div {...props} className={wrapper({ className, variant, size })}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Badge';
|
||||
@@ -0,0 +1,185 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
|
||||
/**
|
||||
* Defines the theme for a button component.
|
||||
*/
|
||||
export const buttonTheme = tv(
|
||||
{
|
||||
base: [
|
||||
'h-fit',
|
||||
'inline-flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'whitespace-nowrap',
|
||||
'focus-ring',
|
||||
'disabled:cursor-not-allowed',
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
shape: {
|
||||
default: '',
|
||||
rounded: 'rounded-full',
|
||||
},
|
||||
iconOnly: {
|
||||
true: '',
|
||||
},
|
||||
variant: {
|
||||
primary: [
|
||||
'text-elements-on-primary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-controls-primary',
|
||||
'shadow-button',
|
||||
'hover:bg-controls-primary-hovered',
|
||||
'focus-visible:bg-controls-primary-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
secondary: [
|
||||
'text-elements-on-secondary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-controls-secondary',
|
||||
'hover:bg-controls-secondary-hovered',
|
||||
'focus-visible:bg-controls-secondary-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
tertiary: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-controls-tertiary',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
ghost: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
danger: [
|
||||
'text-elements-on-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-border-danger',
|
||||
'hover:bg-controls-danger-hovered',
|
||||
'focus-visible:bg-controls-danger-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
'danger-ghost': [
|
||||
'text-elements-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
link: [
|
||||
'p-0',
|
||||
'gap-1.5',
|
||||
'text-elements-link',
|
||||
'rounded',
|
||||
'focus-ring',
|
||||
'hover:underline',
|
||||
'hover:text-elements-link-hovered',
|
||||
'disabled:text-controls-disabled',
|
||||
'disabled:hover:no-underline',
|
||||
],
|
||||
'link-emphasized': [
|
||||
'p-0',
|
||||
'gap-1.5',
|
||||
'text-elements-high-em',
|
||||
'rounded',
|
||||
'underline',
|
||||
'focus-ring',
|
||||
'hover:text-elements-link-hovered',
|
||||
'disabled:text-controls-disabled',
|
||||
'disabled:hover:no-underline',
|
||||
'dark:text-elements-on-high-contrast',
|
||||
],
|
||||
unstyled: [],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
size: 'lg',
|
||||
iconOnly: true,
|
||||
class: ['py-3.5', 'px-3.5'],
|
||||
},
|
||||
{
|
||||
size: 'md',
|
||||
iconOnly: true,
|
||||
class: ['py-3.25', 'px-3.25'],
|
||||
},
|
||||
{
|
||||
size: 'sm',
|
||||
iconOnly: true,
|
||||
class: ['py-2', 'px-2'],
|
||||
},
|
||||
{
|
||||
size: 'xs',
|
||||
iconOnly: true,
|
||||
class: ['py-1', 'px-1'],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
fullWidth: false,
|
||||
iconOnly: false,
|
||||
shape: 'rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
|
||||
*/
|
||||
export type ButtonTheme = VariantProps<typeof buttonTheme>;
|
||||
@@ -0,0 +1,186 @@
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { buttonTheme } from './Button.theme';
|
||||
import type { ButtonTheme } from './Button.theme';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
/**
|
||||
* Represents the properties of a base button component.
|
||||
*/
|
||||
export interface ButtonBaseProps {
|
||||
/**
|
||||
* The optional left icon element for a component.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The optional right icon element to display.
|
||||
* @type {ReactNode}
|
||||
*/
|
||||
rightIcon?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the props of a button link component.
|
||||
*/
|
||||
export interface ButtonLinkProps
|
||||
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
|
||||
/**
|
||||
* Specifies the optional property `as` with a value of `'a'`.
|
||||
* @type {'a'}
|
||||
*/
|
||||
as?: 'a';
|
||||
/**
|
||||
* Indicates whether the item is external or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
external?: boolean;
|
||||
/**
|
||||
* The URL of a web page or resource.
|
||||
* @type {string}
|
||||
*/
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface ButtonProps
|
||||
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
|
||||
/**
|
||||
* Specifies the optional property `as` with a value of `'button'`.
|
||||
* @type {'button'}
|
||||
*/
|
||||
as?: 'button';
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the props for a button component.
|
||||
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
|
||||
*/
|
||||
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
|
||||
ButtonBaseProps &
|
||||
ButtonTheme;
|
||||
|
||||
/**
|
||||
* A custom button component that can be used in React applications.
|
||||
*/
|
||||
const Button = forwardRef<
|
||||
HTMLButtonElement | HTMLAnchorElement,
|
||||
ButtonOrLinkProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
variant,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Conditionally render between <NextLink>, <a> or <button> depending on props
|
||||
// useCallback to prevent unnecessary re-rendering
|
||||
const Component = useCallback(
|
||||
({ children: _children, ..._props }: ButtonOrLinkProps) => {
|
||||
if (_props.as === 'a') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { external, href, as, ...baseLinkProps } = _props;
|
||||
|
||||
// External link
|
||||
if (external) {
|
||||
const externalLinkProps = {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
href,
|
||||
...baseLinkProps,
|
||||
};
|
||||
return (
|
||||
// @ts-expect-error - ref
|
||||
<a ref={ref} {...externalLinkProps}>
|
||||
{_children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Internal link
|
||||
return (
|
||||
// @ts-expect-error - ref
|
||||
<Link ref={ref} {...baseLinkProps} to={href}>
|
||||
{_children}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
const { ...buttonProps } = _props;
|
||||
return (
|
||||
// @ts-expect-error - as prop is not a valid prop for button elements
|
||||
<button ref={ref} {...buttonProps}>
|
||||
{_children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Extracts specific style properties from the given props object and returns them as a new object.
|
||||
*/
|
||||
const styleProps = (({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
iconOnly = false,
|
||||
shape = 'rounded',
|
||||
as,
|
||||
}) => ({
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
as,
|
||||
}))({ ...props, fullWidth, iconOnly, shape, variant });
|
||||
|
||||
/**
|
||||
* Validates that a button component has either children or an aria-label prop.
|
||||
*/
|
||||
if (typeof children === 'undefined' && !props['aria-label']) {
|
||||
throw new Error(
|
||||
'Button components must have either children or an aria-label prop',
|
||||
);
|
||||
}
|
||||
|
||||
const iconSize = useCallback(() => {
|
||||
switch (styleProps.size) {
|
||||
case 'lg':
|
||||
return { width: 20, height: 20 };
|
||||
case 'sm':
|
||||
case 'xs':
|
||||
return { width: 16, height: 16 };
|
||||
case 'md':
|
||||
default: {
|
||||
return { width: 18, height: 18 };
|
||||
}
|
||||
}
|
||||
}, [styleProps.size])();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
className={buttonTheme({ ...styleProps, class: className })}
|
||||
>
|
||||
{cloneIcon(leftIcon, { ...iconSize })}
|
||||
{children}
|
||||
{cloneIcon(rightIcon, { ...iconSize })}
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Button';
|
||||
export * from './Button.theme';
|
||||
@@ -0,0 +1,128 @@
|
||||
/* React Calendar */
|
||||
.react-calendar {
|
||||
@apply border-none font-sans;
|
||||
}
|
||||
|
||||
/* Weekdays -- START */
|
||||
.react-calendar__month-view__weekdays {
|
||||
@apply p-0 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
@apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
text-decoration: none;
|
||||
}
|
||||
/* Weekdays -- END */
|
||||
|
||||
/* Days -- START */
|
||||
.react-calendar__month-view__days {
|
||||
@apply p-0 gap-0;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
@apply !text-elements-disabled;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth:hover {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
@apply !text-elements-disabled !bg-transparent;
|
||||
}
|
||||
|
||||
/* For weekend days */
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
/* color: ${colors.grey[950]} !important; */
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
@apply h-12 w-12 text-elements-high-em;
|
||||
}
|
||||
|
||||
.react-calendar__tile:hover {
|
||||
@apply bg-base-bg-emphasized rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile:focus-visible {
|
||||
@apply bg-base-bg-emphasized rounded-lg focus-ring z-10;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:hover {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:focus-visible {
|
||||
@apply bg-base-bg-emphasized text-elements-high-em rounded-lg focus-ring;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:hover {
|
||||
@apply bg-controls-primary-hovered;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:focus-visible {
|
||||
@apply bg-controls-primary-hovered focus-ring;
|
||||
}
|
||||
|
||||
/* Range -- START */
|
||||
.react-calendar__tile--range {
|
||||
@apply bg-controls-secondary text-elements-on-secondary rounded-none;
|
||||
}
|
||||
|
||||
.react-calendar__tile--range:hover {
|
||||
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none;
|
||||
}
|
||||
|
||||
.react-calendar__tile--range:focus-visible {
|
||||
@apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeStart:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--rangeEnd:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
/* Range -- END */
|
||||
/* Days -- END */
|
||||
|
||||
/* Months -- START */
|
||||
.react-calendar__tile--hasActive {
|
||||
@apply bg-controls-primary text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:hover {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:focus-visible {
|
||||
@apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const calendarTheme = tv({
|
||||
slots: {
|
||||
wrapper: [
|
||||
'max-w-[352px]',
|
||||
'bg-surface-floating',
|
||||
'shadow-dropdown',
|
||||
'rounded-xl',
|
||||
],
|
||||
calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'],
|
||||
navigation: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-between',
|
||||
'gap-3',
|
||||
'py-2.5',
|
||||
'px-1',
|
||||
],
|
||||
actions: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'],
|
||||
button: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'gap-2',
|
||||
'px-2',
|
||||
'py-2',
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'text-elements-high-em',
|
||||
'shadow-field',
|
||||
'bg-white',
|
||||
'hover:bg-base-bg-alternate',
|
||||
'focus-visible:bg-base-bg-alternate',
|
||||
],
|
||||
footer: [
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-end',
|
||||
'py-3',
|
||||
'px-2',
|
||||
'gap-3',
|
||||
'border-t',
|
||||
'border-border-separator',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export type CalendarTheme = VariantProps<typeof calendarTheme>;
|
||||
@@ -0,0 +1,299 @@
|
||||
import React, {
|
||||
ComponentPropsWithRef,
|
||||
MouseEvent,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Calendar as ReactCalendar,
|
||||
CalendarProps as ReactCalendarProps,
|
||||
} from 'react-calendar';
|
||||
import { CalendarTheme, calendarTheme } from './Calendar.theme';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
const CALENDAR_VIEW = ['month', 'year', 'decade'] as const;
|
||||
export type CalendarView = (typeof CALENDAR_VIEW)[number];
|
||||
|
||||
/**
|
||||
* Defines a custom set of props for a React calendar component by excluding specific props
|
||||
* from the original ReactCalendarProps type.
|
||||
* @type {CustomReactCalendarProps}
|
||||
*/
|
||||
type CustomReactCalendarProps = Omit<
|
||||
ReactCalendarProps,
|
||||
'view' | 'showNavigation' | 'onClickMonth' | 'onClickYear'
|
||||
>;
|
||||
|
||||
export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
|
||||
/**
|
||||
* Optional props for wrapping a component with a div element.
|
||||
*/
|
||||
wrapperProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Props for the calendar wrapper component.
|
||||
*/
|
||||
calendarWrapperProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Optional props for the footer component.
|
||||
*/
|
||||
footerProps?: ComponentPropsWithRef<'div'>;
|
||||
/**
|
||||
* Optional custom actions to be rendered.
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Optional callback function that is called when a value is selected.
|
||||
* @param {Value} value - The selected value
|
||||
* @returns None
|
||||
*/
|
||||
onSelect?: (value: Value) => void;
|
||||
/**
|
||||
* Optional callback function that is called when a cancel action is triggered.
|
||||
* @returns None
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar component that allows users to select dates and navigate through months and years.
|
||||
* @param {Object} CalendarProps - Props for the Calendar component.
|
||||
* @returns {JSX.Element} A calendar component with navigation, date selection, and actions.
|
||||
*/
|
||||
export const Calendar = ({
|
||||
selectRange,
|
||||
activeStartDate: activeStartDateProp,
|
||||
value: valueProp,
|
||||
wrapperProps,
|
||||
calendarWrapperProps,
|
||||
footerProps,
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
const {
|
||||
wrapper,
|
||||
calendar,
|
||||
navigation,
|
||||
actions: actionsClass,
|
||||
button,
|
||||
footer,
|
||||
} = calendarTheme();
|
||||
|
||||
const today = new Date();
|
||||
const currentMonth = format(today, 'MMM');
|
||||
const currentYear = format(today, 'yyyy');
|
||||
|
||||
const [view, setView] = useState<CalendarView>('month');
|
||||
const [activeDate, setActiveDate] = useState<Date>(
|
||||
activeStartDateProp ?? today,
|
||||
);
|
||||
const [value, setValue] = useState<Value>(valueProp as Value);
|
||||
const [month, setMonth] = useState(currentMonth);
|
||||
const [year, setYear] = useState(currentYear);
|
||||
|
||||
/**
|
||||
* Update the navigation label based on the active date
|
||||
*/
|
||||
const changeNavigationLabel = useCallback(
|
||||
(date: Date) => {
|
||||
setMonth(format(date, 'MMM'));
|
||||
setYear(format(date, 'yyyy'));
|
||||
},
|
||||
[setMonth, setYear],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the active date base on the action and range
|
||||
*/
|
||||
const handleNavigate = useCallback(
|
||||
(action: 'previous' | 'next', view: CalendarView) => {
|
||||
setActiveDate((date) => {
|
||||
const newDate = new Date(date);
|
||||
switch (view) {
|
||||
case 'month':
|
||||
newDate.setMonth(
|
||||
action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1,
|
||||
);
|
||||
break;
|
||||
case 'year':
|
||||
newDate.setFullYear(
|
||||
action === 'previous'
|
||||
? date.getFullYear() - 1
|
||||
: date.getFullYear() + 1,
|
||||
);
|
||||
break;
|
||||
case 'decade':
|
||||
newDate.setFullYear(
|
||||
action === 'previous'
|
||||
? date.getFullYear() - 10
|
||||
: date.getFullYear() + 10,
|
||||
);
|
||||
break;
|
||||
}
|
||||
changeNavigationLabel(newDate);
|
||||
return newDate;
|
||||
});
|
||||
},
|
||||
[setActiveDate, changeNavigationLabel],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the view of the calendar
|
||||
*/
|
||||
const handleChangeView = useCallback(
|
||||
(view: CalendarView) => {
|
||||
setView(view);
|
||||
},
|
||||
[setView],
|
||||
);
|
||||
|
||||
/**
|
||||
* Change the active date and set the view to the selected type
|
||||
* and also update the navigation label
|
||||
*/
|
||||
const handleChangeNavigation = useCallback(
|
||||
(view: 'month' | 'year', date: Date) => {
|
||||
setActiveDate(date);
|
||||
changeNavigationLabel(date);
|
||||
setView(view);
|
||||
},
|
||||
[setActiveDate, changeNavigationLabel, setView],
|
||||
);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
switch (view) {
|
||||
case 'month':
|
||||
return handleNavigate('previous', 'month');
|
||||
case 'year':
|
||||
return handleNavigate('previous', 'year');
|
||||
case 'decade':
|
||||
return handleNavigate('previous', 'decade');
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
switch (view) {
|
||||
case 'month':
|
||||
return handleNavigate('next', 'month');
|
||||
case 'year':
|
||||
return handleNavigate('next', 'year');
|
||||
case 'decade':
|
||||
return handleNavigate('next', 'decade');
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: Value, event: MouseEvent<HTMLButtonElement>) => {
|
||||
setValue(newValue);
|
||||
|
||||
// Call the onChange prop if it exists
|
||||
onChangeProp?.(newValue, event);
|
||||
|
||||
/**
|
||||
* Update the active date and navigation label
|
||||
*
|
||||
* NOTE:
|
||||
* For range selection, the active date is not updated
|
||||
* The user only can select multiple dates within the same month
|
||||
*/
|
||||
if (!selectRange) {
|
||||
setActiveDate(newValue as Date);
|
||||
changeNavigationLabel(newValue as Date);
|
||||
}
|
||||
},
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={wrapper({ className: wrapperProps?.className })}
|
||||
>
|
||||
{/* Calendar wrapper */}
|
||||
<div
|
||||
{...calendarWrapperProps}
|
||||
className={calendar({ className: calendarWrapperProps?.className })}
|
||||
>
|
||||
{/* Navigation */}
|
||||
<div className={navigation()}>
|
||||
<Button iconOnly size="sm" variant="ghost" onClick={handlePrevious}>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className={actionsClass()}>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={button()}
|
||||
rightIcon={
|
||||
<ChevronGrabberHorizontal className="text-elements-low-em" />
|
||||
}
|
||||
onClick={() => handleChangeView('year')}
|
||||
>
|
||||
{month}
|
||||
</Button>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={button()}
|
||||
rightIcon={
|
||||
<ChevronGrabberHorizontal className="text-elements-low-em" />
|
||||
}
|
||||
onClick={() => handleChangeView('decade')}
|
||||
>
|
||||
{year}
|
||||
</Button>
|
||||
</div>
|
||||
<Button iconOnly size="sm" variant="ghost" onClick={handleNext}>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Calendar */}
|
||||
<ReactCalendar
|
||||
{...props}
|
||||
activeStartDate={activeDate}
|
||||
view={view}
|
||||
value={value}
|
||||
showNavigation={false}
|
||||
selectRange={selectRange}
|
||||
onChange={handleChange}
|
||||
onClickMonth={(date) => handleChangeNavigation('month', date)}
|
||||
onClickYear={(date) => handleChangeNavigation('year', date)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!value}
|
||||
onClick={() => (value ? onSelect?.(value) : null)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Calendar';
|
||||
@@ -0,0 +1,68 @@
|
||||
import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const getCheckboxVariant = tv({
|
||||
slots: {
|
||||
wrapper: ['group', 'flex', 'gap-3'],
|
||||
indicator: [
|
||||
'grid',
|
||||
'place-content-center',
|
||||
'text-transparent',
|
||||
'group-hover:text-controls-disabled',
|
||||
'focus-visible:text-controls-disabled',
|
||||
'group-focus-visible:text-controls-disabled',
|
||||
'data-[state=checked]:text-elements-on-primary',
|
||||
'data-[state=checked]:group-focus-visible:text-elements-on-primary',
|
||||
'data-[state=indeterminate]:text-elements-on-primary',
|
||||
'data-[state=checked]:data-[disabled]:text-elements-on-disabled-active',
|
||||
],
|
||||
icon: ['w-3', 'h-3', 'stroke-current', 'text-current'],
|
||||
input: [
|
||||
'h-5',
|
||||
'w-5',
|
||||
'group',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-controls-tertiary',
|
||||
'rounded-md',
|
||||
'transition-all',
|
||||
'duration-150',
|
||||
'focus-ring',
|
||||
'shadow-button',
|
||||
'group-hover:border-border-interactive/[0.14]',
|
||||
'group-hover:bg-controls-tertiary',
|
||||
'data-[state=checked]:bg-controls-primary',
|
||||
'data-[state=checked]:hover:bg-controls-primary-hovered',
|
||||
'data-[state=checked]:focus-visible:bg-controls-primary-hovered',
|
||||
'data-[disabled]:bg-controls-disabled',
|
||||
'data-[disabled]:shadow-none',
|
||||
'data-[disabled]:hover:border-border-interactive/10',
|
||||
'data-[disabled]:cursor-not-allowed',
|
||||
'data-[state=checked]:data-[disabled]:bg-controls-disabled-active',
|
||||
],
|
||||
label: [
|
||||
'text-sm',
|
||||
'tracking-[-0.006em]',
|
||||
'text-elements-high-em',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-1',
|
||||
'px-1',
|
||||
],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: {
|
||||
wrapper: ['cursor-not-allowed'],
|
||||
indicator: ['group-hover:text-transparent'],
|
||||
input: [
|
||||
'group-hover:border-border-interactive/[0.14]',
|
||||
'group-hover:bg-controls-disabled',
|
||||
],
|
||||
label: ['cursor-not-allowed'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type CheckboxVariants = VariantProps<typeof getCheckboxVariant>;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import * as CheckboxRadix from '@radix-ui/react-checkbox';
|
||||
import { type CheckboxProps as CheckboxRadixProps } from '@radix-ui/react-checkbox';
|
||||
|
||||
import { getCheckboxVariant, type CheckboxVariants } from './Checkbox.theme';
|
||||
import { CheckIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
interface CheckBoxProps extends CheckboxRadixProps, CheckboxVariants {
|
||||
/**
|
||||
* The label of the checkbox.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* The description of the checkbox.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox component is used to allow users to select one or more items from a set.
|
||||
* It is a wrapper around the `@radix-ui/react-checkbox` component.
|
||||
*
|
||||
* It accepts all the props from `@radix-ui/react-checkbox` component and the variants from the theme.
|
||||
*
|
||||
* It also accepts `label` and `description` props to display the label and description of the checkbox.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Checkbox
|
||||
* id="checkbox"
|
||||
* label="Checkbox"
|
||||
* description="This is a checkbox"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const Checkbox = ({
|
||||
id,
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: CheckBoxProps) => {
|
||||
const {
|
||||
wrapper: wrapperStyle,
|
||||
indicator: indicatorStyle,
|
||||
icon: iconStyle,
|
||||
input: inputStyle,
|
||||
label: labelStyle,
|
||||
description: descriptionStyle,
|
||||
} = getCheckboxVariant({
|
||||
disabled: props?.disabled,
|
||||
});
|
||||
return (
|
||||
<div className={wrapperStyle()}>
|
||||
<CheckboxRadix.Root
|
||||
{...props}
|
||||
className={inputStyle({ className })}
|
||||
id={id}
|
||||
onCheckedChange={onCheckedChange}
|
||||
>
|
||||
<CheckboxRadix.Indicator forceMount className={indicatorStyle()}>
|
||||
<CheckIcon className={iconStyle()} />
|
||||
</CheckboxRadix.Indicator>
|
||||
</CheckboxRadix.Root>
|
||||
{label && (
|
||||
<label className={labelStyle()} htmlFor={id}>
|
||||
{label}
|
||||
{description && (
|
||||
<span className={descriptionStyle()}>{description}</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Checkbox';
|
||||
@@ -30,23 +30,31 @@ const ConfirmDialog = ({
|
||||
color,
|
||||
}: ConfirmDialogProp) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<Typography variant="h6">{dialogTitle} </Typography>
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
{dialogTitle}{' '}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className=" rounded-full"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<DialogBody>{children}</DialogBody>
|
||||
<DialogFooter className="flex justify-start gap-2">
|
||||
<Button variant="outlined" onClick={handleOpen}>
|
||||
<DialogBody placeholder={''}>{children}</DialogBody>
|
||||
<DialogFooter className="flex justify-start gap-2" placeholder={''}>
|
||||
<Button variant="outlined" onClick={handleOpen} placeholder={''}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="gradient" color={color} onClick={handleConfirm}>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color={color}
|
||||
onClick={handleConfirm}
|
||||
placeholder={''}
|
||||
>
|
||||
{confirmButtonTitle}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.5 7.5L4.64706 10L10.5 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRadioIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5ZM7.93079 12.3663L12.3055 7.01944L11.1446 6.06958L7.81944 10.1336L6.37511 8.68932L5.31445 9.74998L7.93079 12.3663Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronDownIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20 9L13.4142 15.5858C12.6332 16.3668 11.3669 16.3668 10.5858 15.5858L4 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronGrabberHorizontal = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.66666 12.5L9.99999 15.8333L13.3333 12.5M6.66666 7.5L9.99999 4.16666L13.3333 7.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronLeft = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.7071 2.66666L5.37378 8.00001L10.7071 13.3333L10 14.0404L3.95956 8.00001L10 1.95956L10.7071 2.66666Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const ChevronRight = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.00001 1.95956L12.0405 7.99998L6.00001 14.0404L5.29291 13.3333L10.6262 7.99999L5.29291 2.66666L6.00001 1.95956Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CrossIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5 5L19 19M19 5L5 19"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
|
||||
size?: number | string; // width and height will both be set as the same value
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const CustomIcon = ({
|
||||
children,
|
||||
width = 24,
|
||||
height = 24,
|
||||
size,
|
||||
viewBox = '0 0 24 24',
|
||||
name,
|
||||
...rest
|
||||
}: CustomIconProps) => {
|
||||
return (
|
||||
<svg
|
||||
aria-labelledby={name}
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox={viewBox}
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const EllipseIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon width="9" height="8" viewBox="0 0 9 8" fill="none" {...props}>
|
||||
<circle cx="4.5" cy="4" r="4" fill="#082F56" fillOpacity="0.1" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
import { EllipseIcon } from './EllipseIcon';
|
||||
|
||||
export const EllipsesIcon: React.FC<CustomIconProps> = (props) => {
|
||||
return (
|
||||
<CustomIcon width="51" height="8" viewBox="0 0 51 8" fill="none" {...props}>
|
||||
<EllipseIcon />
|
||||
<EllipseIcon x="14" />
|
||||
<EllipseIcon x="28" />
|
||||
<EllipseIcon x="42" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const FolderIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
id="Icon"
|
||||
d="M2.0835 4.58341V14.5834C2.0835 15.5039 2.82969 16.2501 3.75016 16.2501H16.2502C17.1706 16.2501 17.9168 15.5039 17.9168 14.5834V7.08341C17.9168 6.16294 17.1706 5.41675 16.2502 5.41675H10.8921C10.3349 5.41675 9.81449 5.13825 9.50539 4.67458L8.82827 3.65891C8.51916 3.19525 7.99878 2.91675 7.44152 2.91675H3.75016C2.82969 2.91675 2.0835 3.66294 2.0835 4.58341Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user