Merge pull request #107 from snowball-tools/designsystem

merge design system into main
This commit is contained in:
Vivian Phung 2024-02-26 17:50:01 -05:00 committed by GitHub
commit 48fd953c60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
184 changed files with 9215 additions and 3642 deletions

5
.gitignore vendored
View File

@ -1 +1,6 @@
node_modules/ node_modules/
yarn-error.log
.yarnrc.yml
.yarn/
packages/backend/environments/local.toml

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

View File

@ -28,6 +28,12 @@
cd packages/backend 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) - 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) - 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` - In "Homepage URL", type `http://localhost:3000`
@ -181,6 +187,12 @@
cd packages/frontend 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 - Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env ```env

View File

@ -25,6 +25,9 @@
"allowArgumentsExplicitlyTypedAsAny": true "allowArgumentsExplicitlyTypedAsAny": true
} }
], ],
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] "@typescript-eslint/no-unused-vars": [
"error",
{ "ignoreRestSiblings": true }
]
} }
} }

View File

@ -1,2 +1,3 @@
db db
dist dist
environments/local.toml

View File

@ -24,7 +24,7 @@ export interface GitHubConfig {
oAuth: { oAuth: {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
} };
} }
export interface RegistryConfig { export interface RegistryConfig {
@ -39,7 +39,7 @@ export interface RegistryConfig {
amount: string; amount: string;
denom: string; denom: string;
gas: string; gas: string;
} };
} }
export interface MiscConfig { export interface MiscConfig {

View File

@ -1,5 +1,6 @@
import process from 'process'; 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'; export const DEFAULT_GQL_PATH = '/graphql';

View File

@ -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 path from 'path';
import debug from 'debug'; import debug from 'debug';
import assert from 'assert'; import assert from 'assert';
@ -74,14 +80,18 @@ export class Database {
return updateResult.affected > 0; 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 organizationRepository = this.dataSource.getRepository(Organization);
const organizations = await organizationRepository.find(options); const organizations = await organizationRepository.find(options);
return organizations; return organizations;
} }
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> { async getOrganization (
options: FindOneOptions<Organization>
): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options); const organization = await organizationRepository.findOne(options);
@ -123,7 +133,11 @@ export class Database {
const project = await projectRepository const project = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.createdBy', 'user') .leftJoinAndSelect('deployments.createdBy', 'user')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoinAndSelect('project.owner', 'owner') .leftJoinAndSelect('project.owner', 'owner')
@ -136,19 +150,29 @@ export class Database {
return project; 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 projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect(
'project.deployments',
'deployments',
'deployments.isCurrent = true'
)
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.leftJoin('project.organization', 'organization') .leftJoin('project.organization', 'organization')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', { .where(
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
{
userId, userId,
organizationSlug organizationSlug
}) }
)
.getMany(); .getMany();
return projects; return projects;
@ -157,7 +181,9 @@ export class Database {
/** /**
* Get deployments with specified filter * 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 deploymentRepository = this.dataSource.getRepository(Deployment);
const deployments = await deploymentRepository.find(options); 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 deploymentRepository = this.dataSource.getRepository(Deployment);
const deployment = await deploymentRepository.findOne(options); const deployment = await deploymentRepository.findOne(options);
@ -210,8 +238,11 @@ export class Database {
return deployment; return deployment;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectId: string
): Promise<ProjectMember[]> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMembers = await projectMemberRepository.find({ const projectMembers = await projectMemberRepository.find({
relations: { relations: {
@ -228,8 +259,12 @@ export class Database {
return projectMembers; return projectMembers;
} }
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); projectId: string,
filter?: FindOptionsWhere<EnvironmentVariable>
): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const environmentVariables = await environmentVariableRepository.find({ const environmentVariables = await environmentVariableRepository.find({
where: { where: {
@ -244,9 +279,12 @@ export class Database {
} }
async removeProjectMemberById (projectMemberId: string): Promise<boolean> { 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) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -255,37 +293,63 @@ export class Database {
} }
} }
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> { async updateProjectMemberById (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); projectMemberId: string,
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data); data: DeepPartial<ProjectMember>
): Promise<boolean> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const updateResult = await projectMemberRepository.update(
{ id: projectMemberId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> { async addProjectMember (
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); data: DeepPartial<ProjectMember>
): Promise<ProjectMember> {
const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const newProjectMember = await projectMemberRepository.save(data); const newProjectMember = await projectMemberRepository.save(data);
return newProjectMember; return newProjectMember;
} }
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> { async addEnvironmentVariables (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); data: DeepPartial<EnvironmentVariable>[]
const savedEnvironmentVariables = await environmentVariableRepository.save(data); ): Promise<EnvironmentVariable[]> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const savedEnvironmentVariables =
await environmentVariableRepository.save(data);
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string,
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data); data: DeepPartial<EnvironmentVariable>
): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update(
{ id: environmentVariableId },
data
);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async deleteEnvironmentVariable (
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); environmentVariableId: string
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId }); ): Promise<boolean> {
const environmentVariableRepository =
this.dataSource.getRepository(EnvironmentVariable);
const deleteResult = await environmentVariableRepository.delete({
id: environmentVariableId
});
if (deleteResult.affected) { if (deleteResult.affected) {
return deleteResult.affected > 0; return deleteResult.affected > 0;
@ -295,7 +359,8 @@ export class Database {
} }
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> { async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const projectMemberRepository =
this.dataSource.getRepository(ProjectMember);
const projectMemberWithProject = await projectMemberRepository.find({ const projectMemberWithProject = await projectMemberRepository.find({
relations: { relations: {
@ -307,8 +372,7 @@ export class Database {
where: { where: {
id: projectMemberId id: projectMemberId
} }
} });
);
if (projectMemberWithProject.length === 0) { if (projectMemberWithProject.length === 0) {
throw new Error('Member does not exist'); throw new Error('Member does not exist');
@ -317,34 +381,49 @@ export class Database {
return projectMemberWithProject[0]; 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 projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
.createQueryBuilder('project') .createQueryBuilder('project')
.leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.organization', 'organization')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', { .where(
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
{
userId, userId,
searchText: `%${searchText}%` searchText: `%${searchText}%`
}) }
)
.getMany(); .getMany();
return projects; 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); 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 deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository.update(criteria, data); const updateResult = await deploymentRepository.update(criteria, data);
return Boolean(updateResult.affected); 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 deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository const updateResult = await deploymentRepository
@ -378,9 +457,15 @@ export class Database {
return projectRepository.save(newProject); 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 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); return Boolean(updateResult.affected);
} }
@ -427,14 +512,20 @@ export class Database {
return domain; 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 domainRepository = this.dataSource.getRepository(Domain);
const updateResult = await domainRepository.update({ id: domainId }, data); const updateResult = await domainRepository.update({ id: domainId }, data);
return Boolean(updateResult.affected); 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 domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find({ const domains = await domainRepository.find({

View File

@ -27,26 +27,26 @@ export enum DeploymentStatus {
} }
export interface ApplicationDeploymentRequest { export interface ApplicationDeploymentRequest {
type: string type: string;
version: string version: string;
name: string name: string;
application: string application: string;
config: string, config: string;
meta: string meta: string;
} }
export interface ApplicationRecord { export interface ApplicationRecord {
type: string; type: string;
version:string version: string;
name: string name: string;
description?: string description?: string;
homepage?: string homepage?: string;
license?: string license?: string;
author?: string author?: string;
repository?: string[], repository?: string[];
app_version?: string app_version?: string;
repository_ref: string repository_ref: string;
app_type: string app_type: string;
} }
@Entity() @Entity()

View File

@ -27,8 +27,12 @@ export class Organization {
@UpdateDateColumn() @UpdateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, { @OneToMany(
() => UserOrganization,
(userOrganization) => userOrganization.organization,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -76,7 +76,7 @@ export class Project {
@OneToMany(() => Deployment, (deployment) => deployment.project) @OneToMany(() => Deployment, (deployment) => deployment.project)
deployments!: Deployment[]; deployments!: Deployment[];
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];

View File

@ -15,7 +15,7 @@ import { User } from './User';
export enum Permission { export enum Permission {
View = 'View', View = 'View',
Edit = 'Edit' Edit = 'Edit',
} }
@Entity() @Entity()

View File

@ -39,13 +39,17 @@ export class User {
@CreateDateColumn() @CreateDateColumn()
updatedAt!: Date; updatedAt!: Date;
@OneToMany(() => ProjectMember, projectMember => projectMember.project, { @OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
cascade: ['soft-remove'] cascade: ['soft-remove']
}) })
projectMembers!: ProjectMember[]; projectMembers!: ProjectMember[];
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, { @OneToMany(
() => UserOrganization,
(UserOrganization) => UserOrganization.member,
{
cascade: ['soft-remove'] cascade: ['soft-remove']
}) }
)
userOrganizations!: UserOrganization[]; userOrganizations!: UserOrganization[];
} }

View File

@ -31,9 +31,16 @@ export const main = async (): Promise<void> => {
await db.init(); await db.init();
const registry = new Registry(registryConfig); 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); const resolvers = await createResolvers(service);
await createAndStartServer(server, typeDefs, resolvers, service); await createAndStartServer(server, typeDefs, resolvers, service);

View File

@ -6,7 +6,11 @@ import { DateTime } from 'luxon';
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
import { RegistryConfig } from './config'; import { RegistryConfig } from './config';
import { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment'; import {
ApplicationRecord,
Deployment,
ApplicationDeploymentRequest
} from './entity/Deployment';
import { AppDeploymentRecord, PackageJSON } from './types'; import { AppDeploymentRecord, PackageJSON } from './types';
const log = debug('snowball:registry'); const log = debug('snowball:registry');
@ -20,9 +24,13 @@ export class Registry {
private registry: LaconicRegistry; private registry: LaconicRegistry;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
constructor (registryConfig : RegistryConfig) { constructor (registryConfig: RegistryConfig) {
this.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 ({ async createApplicationRecord ({
@ -32,24 +40,38 @@ export class Registry {
appType, appType,
repoUrl repoUrl
}: { }: {
appName: string, appName: string;
packageJSON: PackageJSON packageJSON: PackageJSON;
commitHash: string, commitHash: string;
appType: string, appType: string;
repoUrl: string repoUrl: string;
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { }): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
// Use laconic-sdk to publish record // 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 // Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
// Fetch previous records // Fetch previous records
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_RECORD_TYPE, type: APP_RECORD_TYPE,
name: packageJSON.name name: packageJSON.name
}, true); },
true
);
// Get next version of record // Get next version of record
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId); const bondRecords = records.filter(
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); (record: any) => record.bondId === this.registryConfig.bondId
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch'); );
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'); assert(nextVersion, 'Application record version not valid');
@ -64,7 +86,12 @@ export class Registry {
...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }), ...(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 }) ...(packageJSON.version && { app_version: packageJSON.version })
}; };
@ -84,11 +111,29 @@ export class Registry {
const crn = this.getCrn(appName); const crn = this.getCrn(appName);
log(`Setting name: ${crn} for record ID: ${result.data.id}`); 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(
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); { cid: result.data.id, crn },
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); 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: { async createApplicationDeploymentRequest (data: {
@ -98,8 +143,8 @@ export class Registry {
repository: string, repository: string,
environmentVariables: { [key: string]: string } environmentVariables: { [key: string]: string }
}): Promise<{ }): Promise<{
applicationDeploymentRequestId: string, applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> { }> {
const crn = this.getCrn(data.appName); const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]); const records = await this.registry.resolveNames([crn]);
@ -125,7 +170,9 @@ export class Registry {
env: data.environmentVariables env: data.environmentVariables
}), }),
meta: JSON.stringify({ 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: data.repository,
repository_ref: data.deployment.commitHash 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 record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest); log('Application deployment request data:', applicationDeploymentRequest);
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; return {
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
} }
/** /**
* Fetch ApplicationDeploymentRecords for deployments * Fetch ApplicationDeploymentRecords for deployments
*/ */
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> { async getDeploymentRecords (
deployments: Deployment[]
): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
const records = await this.registry.queryRecords({ const records = await this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE type: APP_DEPLOYMENT_RECORD_TYPE
}, true); },
true
);
// Filter records with ApplicationRecord ids // 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 { getCrn (appName: string): string {

View File

@ -33,7 +33,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getDeploymentsByProjectId(projectId); return service.getDeploymentsByProjectId(projectId);
}, },
environmentVariables: async (_: any, { projectId }: { projectId: string }) => { environmentVariables: async (
_: any,
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId); return service.getEnvironmentVariablesByProjectId(projectId);
}, },
@ -45,14 +48,24 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.searchProjects(context.user, searchText); 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); return service.getDomainsByProjectId(projectId, filter);
} }
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response
Mutation: { Mutation: {
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => { removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any
) => {
try { try {
return await service.removeProjectMember(context.user, projectMemberId); return await service.removeProjectMember(context.user, projectMemberId);
} catch (err) { } catch (err) {
@ -61,12 +74,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateProjectMember: async (_: any, { projectMemberId, data }: { updateProjectMember: async (
projectMemberId: string, _: any,
{
projectMemberId,
data
}: {
projectMemberId: string;
data: { data: {
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return await service.updateProjectMember(projectMemberId, data); return await service.updateProjectMember(projectMemberId, data);
} catch (err) { } catch (err) {
@ -75,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addProjectMember: async (_: any, { projectId, data }: { addProjectMember: async (
projectId: string, _: any,
{
projectId,
data
}: {
projectId: string;
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
};
} }
}) => { ) => {
try { try {
return Boolean(await service.addProjectMember(projectId, data)); return Boolean(await service.addProjectMember(projectId, data));
} catch (err) { } catch (err) {
@ -90,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 { try {
return Boolean(await service.addEnvironmentVariables(projectId, data)); return Boolean(
await service.addEnvironmentVariables(projectId, data)
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => { updateEnvironmentVariable: async (
_: any,
{
environmentVariableId,
data
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
}
) => {
try { try {
return await service.updateEnvironmentVariable(environmentVariableId, data); return await service.updateEnvironmentVariable(
environmentVariableId,
data
);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => { removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string }
) => {
try { try {
return await service.removeEnvironmentVariable(environmentVariableId); return await service.removeEnvironmentVariable(environmentVariableId);
} catch (err) { } catch (err) {
@ -117,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 { try {
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId)); return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
} catch (err) { } catch (err) {
@ -126,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 { try {
return await service.addProject(context.user, organizationSlug, data); return await service.addProject(context.user, organizationSlug, data);
} catch (err) { } catch (err) {
@ -135,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 { try {
return await service.updateProject(projectId, data); return await service.updateProject(projectId, data);
} catch (err) { } catch (err) {
@ -144,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 { try {
return Boolean(await service.redeployToProd(context.user, deploymentId)); return Boolean(await service.redeployToProd(context.user, deploymentId));
} catch (err) { } catch (err) {
@ -157,7 +226,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
try { try {
return await service.deleteProject(projectId); return await service.deleteProject(projectId);
} catch (err) { } catch (err) {
log(err); return false; log(err);
return false;
} }
}, },
@ -170,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 { try {
return await service.rollbackDeployment(projectId, deploymentId); return await service.rollbackDeployment(projectId, deploymentId);
} catch (err) { } catch (err) {
@ -179,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 { try {
return Boolean(await service.addDomain(projectId, data)); return Boolean(await service.addDomain(projectId, data));
} catch (err) { } catch (err) {
@ -188,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 { try {
return await service.updateDomain(domainId, data); return await service.updateDomain(domainId, data);
} catch (err) { } catch (err) {
@ -197,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 { try {
return await service.authenticateGitHub(code, context.user); return await service.authenticateGitHub(code, context.user);
} catch (err) { } catch (err) {

View File

@ -188,10 +188,19 @@ type Query {
type Mutation { type Mutation {
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean! addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean! updateProjectMember(
projectMemberId: String!
data: UpdateProjectMemberInput
): Boolean!
removeProjectMember(projectMemberId: String!): Boolean! removeProjectMember(projectMemberId: String!): Boolean!
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean! addEnvironmentVariables(
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean! projectId: String!
data: [AddEnvironmentVariableInput!]
): Boolean!
updateEnvironmentVariable(
environmentVariableId: String!
data: UpdateEnvironmentVariableInput!
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project! addProject(organizationSlug: String!, data: AddProjectInput): Project!

View File

@ -23,8 +23,8 @@ const log = debug('snowball:service');
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
interface Config { interface Config {
gitHubConfig: GitHubConfig gitHubConfig: GitHubConfig;
registryConfig: RegistryConfig registryConfig: RegistryConfig;
} }
export class Service { export class Service {
@ -72,7 +72,9 @@ export class Service {
}); });
if (deployments.length) { 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 // Fetch ApplicationDeploymentRecord for deployments
const records = await this.registry.getDeploymentRecords(deployments); const records = await this.registry.getDeploymentRecords(deployments);
@ -92,10 +94,12 @@ export class Service {
/** /**
* Update deployments with ApplicationDeploymentRecord data * Update deployments with ApplicationDeploymentRecord data
*/ */
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> { async updateDeploymentsWithRecordData (
records: AppDeploymentRecord[]
): Promise<void> {
// Get deployments for ApplicationDeploymentRecords // Get deployments for ApplicationDeploymentRecords
const deployments = await this.db.getDeployments({ const deployments = await this.db.getDeployments({
where: records.map(record => ({ where: records.map((record) => ({
applicationRecordId: record.attributes.application applicationRecordId: record.attributes.application
})), })),
order: { order: {
@ -104,38 +108,46 @@ export class Service {
}); });
// Get project IDs of deployments that are in production environment // Get project IDs of deployments that are in production environment
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => { const productionDeploymentProjectIds = deployments.reduce(
(acc, deployment): Set<string> => {
if (deployment.environment === Environment.Production) { if (deployment.environment === Environment.Production) {
acc.add(deployment.projectId); acc.add(deployment.projectId);
} }
return acc; return acc;
}, new Set<string>()); },
new Set<string>()
);
// Set old deployments isCurrent to false // 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) => { const recordToDeploymentsMap = deployments.reduce(
(acc: { [key: string]: Deployment }, deployment) => {
acc[deployment.applicationRecordId] = deployment; acc[deployment.applicationRecordId] = deployment;
return acc; return acc;
}, {}); },
{}
);
// Update deployment data for ApplicationDeploymentRecords // Update deployment data for ApplicationDeploymentRecords
const deploymentUpdatePromises = records.map(async (record) => { const deploymentUpdatePromises = records.map(async (record) => {
const deployment = recordToDeploymentsMap[record.attributes.application]; const deployment = recordToDeploymentsMap[record.attributes.application];
await this.db.updateDeploymentById( await this.db.updateDeploymentById(deployment.id, {
deployment.id,
{
applicationDeploymentRecordId: record.id, applicationDeploymentRecordId: record.id,
applicationDeploymentRecordData: record.attributes, applicationDeploymentRecordData: record.attributes,
url: record.attributes.url, url: record.attributes.url,
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
isCurrent: deployment.environment === Environment.Production 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); await Promise.all(deploymentUpdatePromises);
@ -181,7 +193,10 @@ export class Service {
async getOctokit (userId: string): Promise<Octokit> { async getOctokit (userId: string): Promise<Octokit> {
const user = await this.db.getUser({ where: { id: userId } }); 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 }); return new Octokit({ auth: user.gitHubToken });
} }
@ -206,13 +221,19 @@ export class Service {
return dbDeployments; return dbDeployments;
} }
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> { async getEnvironmentVariablesByProjectId (
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId); projectId: string
): Promise<EnvironmentVariable[]> {
const dbEnvironmentVariables =
await this.db.getEnvironmentVariablesByProjectId(projectId);
return dbEnvironmentVariables; return dbEnvironmentVariables;
} }
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> { async getProjectMembersByProjectId (
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId); projectId: string
): Promise<ProjectMember[]> {
const dbProjectMembers =
await this.db.getProjectMembersByProjectId(projectId);
return dbProjectMembers; return dbProjectMembers;
} }
@ -221,20 +242,28 @@ export class Service {
return dbProjects; 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); const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
return dbDomains; 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); return this.db.updateProjectMemberById(projectMemberId, data);
} }
async addProjectMember (projectId: string, async addProjectMember (
projectId: string,
data: { data: {
email: string, email: string;
permissions: Permission[] permissions: Permission[];
}): Promise<ProjectMember> { }
): Promise<ProjectMember> {
// TODO: Send invitation // TODO: Send invitation
let user = await this.db.getUser({ let user = await this.db.getUser({
where: { where: {
@ -279,29 +308,41 @@ export class Service {
} }
} }
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> { async addEnvironmentVariables (
const formattedEnvironmentVariables = data.map((environmentVariable) => { projectId: string,
data: { environments: string[]; key: string; value: string }[]
): Promise<EnvironmentVariable[]> {
const formattedEnvironmentVariables = data
.map((environmentVariable) => {
return environmentVariable.environments.map((environment) => { return environmentVariable.environments.map((environment) => {
return ({ return {
key: environmentVariable.key, key: environmentVariable.key,
value: environmentVariable.value, value: environmentVariable.value,
environment: environment as Environment, environment: environment as Environment,
project: Object.assign(new Project(), { project: Object.assign(new Project(), {
id: projectId id: projectId
}) })
};
}); });
}); })
}).flat(); .flat();
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables); const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
formattedEnvironmentVariables
);
return savedEnvironmentVariables; 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); return this.db.updateEnvironmentVariable(environmentVariableId, data);
} }
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> { async removeEnvironmentVariable (
environmentVariableId: string
): Promise<boolean> {
return this.db.deleteEnvironmentVariable(environmentVariableId); return this.db.deleteEnvironmentVariable(environmentVariableId);
} }
@ -317,7 +358,10 @@ export class Service {
throw new Error('Deployment does not exist'); 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); const octokit = await this.getOctokit(user.id);
@ -342,7 +386,9 @@ export class Service {
recordData: { repoUrl?: string } = {} recordData: { repoUrl?: string } = {}
): Promise<Deployment> { ): Promise<Deployment> {
assert(data.project?.repository, 'Project repository not found'); 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 [owner, repo] = data.project.repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({ const { data: packageJSONData } = await octokit.rest.repos.getContent({
@ -362,12 +408,16 @@ export class Service {
assert(packageJSON.name, "name field doesn't exist in package.json"); assert(packageJSON.name, "name field doesn't exist in package.json");
if (!recordData.repoUrl) { 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; recordData.repoUrl = repoDetails.html_url;
} }
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record) // TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({ const { applicationRecordId, applicationRecordData } =
await this.registry.createApplicationRecord({
appName: repo, appName: repo,
packageJSON, packageJSON,
appType: data.project!.template!, appType: data.project!.template!,
@ -439,7 +489,9 @@ export class Service {
const octokit = await this.getOctokit(user.id); const octokit = await this.getOctokit(user.id);
const [owner, repo] = project.repository.split('/'); const [owner, repo] = project.repository.split('/');
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({ const {
data: [latestCommit]
} = await octokit.rest.repos.listCommits({
owner, owner,
repo, repo,
sha: project.prodBranch, sha: project.prodBranch,
@ -476,7 +528,10 @@ export class Service {
owner, owner,
repo, repo,
config: { 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' content_type: 'json'
}, },
events: ['push'] events: ['push']
@ -484,9 +539,13 @@ export class Service {
} catch (err) { } catch (err) {
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes // https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
if ( if (
!(err instanceof RequestError && !(
err instanceof RequestError &&
err.status === 422 && err.status === 422 &&
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR)) (err.response?.data as any).errors.some(
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
)
)
) { ) {
throw err; throw err;
} }
@ -498,7 +557,9 @@ export class Service {
async handleGitHubPush (data: GitPushEventPayload): Promise<void> { async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
const { repository, ref, head_commit: headCommit } = data; const { repository, ref, head_commit: headCommit } = data;
log(`Handling GitHub push event from repository: ${repository.full_name}`); 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) { if (!projects.length) {
log(`No projects found for repository ${repository.full_name}`); log(`No projects found for repository ${repository.full_name}`);
@ -510,15 +571,18 @@ export class Service {
for await (const project of projects) { for await (const project of projects) {
const octokit = await this.getOctokit(project.ownerId); 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 // Create deployment with branch and latest commit in GitHub data
await this.createDeployment(project.ownerId, await this.createDeployment(project.ownerId, octokit, {
octokit,
{
project, project,
branch, branch,
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview, environment:
project.prodBranch === branch
? Environment.Production
: Environment.Preview,
domain, domain,
commitHash: headCommit.id, commitHash: headCommit.id,
commitMessage: headCommit.message commitMessage: headCommit.message
@ -526,7 +590,10 @@ export class Service {
} }
} }
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProject (
projectId: string,
data: DeepPartial<Project>
): Promise<boolean> {
return this.db.updateProjectById(projectId, data); return this.db.updateProjectById(projectId, data);
} }
@ -543,7 +610,9 @@ export class Service {
}); });
if (domainsRedirectedFrom.length > 0) { 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); return this.db.deleteDomainById(domainId);
@ -582,7 +651,10 @@ export class Service {
return newDeployment; return newDeployment;
} }
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> { async rollbackDeployment (
projectId: string,
deploymentId: string
): Promise<boolean> {
// TODO: Implement transactions // TODO: Implement transactions
const oldCurrentDeployment = await this.db.getDeployment({ const oldCurrentDeployment = await this.db.getDeployment({
relations: { relations: {
@ -600,16 +672,25 @@ export class Service {
throw new Error('Current deployment doesnot exist'); 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; return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
} }
async addDomain (projectId: string, data: { name: string }): Promise<{ async addDomain (
primaryDomain: Domain, projectId: string,
redirectedDomain: Domain data: { name: string }
): Promise<{
primaryDomain: Domain;
redirectedDomain: Domain;
}> { }> {
const currentProject = await this.db.getProjectById(projectId); const currentProject = await this.db.getProjectById(projectId);
@ -634,12 +715,20 @@ export class Service {
redirectTo: savedPrimaryDomain 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({ const domain = await this.db.getDomain({
where: { where: {
id: domainId id: domainId
@ -680,7 +769,9 @@ export class Service {
} }
if (redirectedDomain.redirectToId) { 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; newDomain.redirectTo = redirectedDomain;

View File

@ -47,5 +47,5 @@ interface RegistryRecord {
} }
export interface AppDeploymentRecord extends RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord {
attributes: AppDeploymentRecordAttributes attributes: AppDeploymentRecordAttributes;
} }

View File

@ -37,10 +37,15 @@ export const getEntities = async (filePath: string): Promise<any> => {
return entities; 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 entityRepository = dataSource.getRepository(entityType);
const savedEntity:Entity[] = []; const savedEntity: Entity[] = [];
for (const entityData of entities) { for (const entityData of entities) {
let entity = entityRepository.create(entityData as DeepPartial<Entity>); let entity = entityRepository.create(entityData as DeepPartial<Entity>);

View File

@ -18,4 +18,4 @@ const main = async () => {
deleteFile(config.database.dbPath); deleteFile(config.database.dbPath);
}; };
main().catch(err => log(err)); main().catch((err) => log(err));

View File

@ -1,9 +1,9 @@
[ [
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":0, "domainIndex": 0,
"createdByIndex": 0, "createdByIndex": 0,
"id":"ffhae3zq", "id": "ffhae3zq",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
@ -18,9 +18,9 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":1, "domainIndex": 1,
"createdByIndex": 0, "createdByIndex": 0,
"id":"vehagei8", "id": "vehagei8",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
@ -35,9 +35,9 @@
}, },
{ {
"projectIndex": 0, "projectIndex": 0,
"domainIndex":2, "domainIndex": 2,
"createdByIndex": 0, "createdByIndex": 0,
"id":"qmgekyte", "id": "qmgekyte",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
@ -54,7 +54,7 @@
"projectIndex": 0, "projectIndex": 0,
"domainIndex": null, "domainIndex": null,
"createdByIndex": 0, "createdByIndex": 0,
"id":"f8wsyim6", "id": "f8wsyim6",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": false, "isCurrent": false,
@ -69,9 +69,9 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":3, "domainIndex": 3,
"createdByIndex": 1, "createdByIndex": 1,
"id":"eO8cckxk", "id": "eO8cckxk",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
@ -86,9 +86,9 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":4, "domainIndex": 4,
"createdByIndex": 1, "createdByIndex": 1,
"id":"yaq0t5yw", "id": "yaq0t5yw",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
@ -103,9 +103,9 @@
}, },
{ {
"projectIndex": 1, "projectIndex": 1,
"domainIndex":5, "domainIndex": 5,
"createdByIndex": 1, "createdByIndex": 1,
"id":"hwwr6sbx", "id": "hwwr6sbx",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
@ -120,9 +120,9 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":9, "domainIndex": 9,
"createdByIndex": 2, "createdByIndex": 2,
"id":"ndxje48a", "id": "ndxje48a",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,
@ -137,9 +137,9 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":7, "domainIndex": 7,
"createdByIndex": 2, "createdByIndex": 2,
"id":"gtgpgvei", "id": "gtgpgvei",
"status": "Ready", "status": "Ready",
"environment": "Preview", "environment": "Preview",
"isCurrent": false, "isCurrent": false,
@ -154,9 +154,9 @@
}, },
{ {
"projectIndex": 2, "projectIndex": 2,
"domainIndex":8, "domainIndex": 8,
"createdByIndex": 2, "createdByIndex": 2,
"id":"b4bpthjr", "id": "b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Development", "environment": "Development",
"isCurrent": false, "isCurrent": false,
@ -173,7 +173,7 @@
"projectIndex": 3, "projectIndex": 3,
"domainIndex": 6, "domainIndex": 6,
"createdByIndex": 2, "createdByIndex": 2,
"id":"b4bpthjr", "id": "b4bpthjr",
"status": "Ready", "status": "Ready",
"environment": "Production", "environment": "Production",
"isCurrent": true, "isCurrent": true,

View File

@ -2,77 +2,55 @@
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 0, "projectIndex": 0,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 1, "projectIndex": 1,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 2, "projectIndex": 2,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 0, "memberIndex": 0,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 3, "projectIndex": 3,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 1, "memberIndex": 1,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View"],
"View"
],
"isPending": false "isPending": false
}, },
{ {
"memberIndex": 2, "memberIndex": 2,
"projectIndex": 4, "projectIndex": 4,
"permissions": [ "permissions": ["View", "Edit"],
"View",
"Edit"
],
"isPending": false "isPending": false
} }
] ]

View File

@ -10,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
import { Domain } from '../src/entity/Domain'; import { Domain } from '../src/entity/Domain';
import { ProjectMember } from '../src/entity/ProjectMember'; import { ProjectMember } from '../src/entity/ProjectMember';
import { Deployment } from '../src/entity/Deployment'; import { Deployment } from '../src/entity/Deployment';
import { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils'; import {
checkFileExists,
getConfig,
getEntities,
loadAndSaveData
} from '../src/utils';
import { Config } from '../src/config'; import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
@ -27,19 +32,34 @@ const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
const generateTestData = async (dataSource: DataSource) => { 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 savedUsers = await loadAndSaveData(User, dataSource, userEntities);
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); const orgEntities = await getEntities(
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities); path.resolve(__dirname, ORGANIZATION_DATA_PATH)
);
const savedOrgs = await loadAndSaveData(
Organization,
dataSource,
orgEntities
);
const projectRelations = { const projectRelations = {
owner: savedUsers, owner: savedUsers,
organization: savedOrgs organization: savedOrgs
}; };
const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH)); const projectEntities = await getEntities(
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations); path.resolve(__dirname, PROJECT_DATA_PATH)
);
const savedProjects = await loadAndSaveData(
Project,
dataSource,
projectEntities,
projectRelations
);
const domainRepository = dataSource.getRepository(Domain); const domainRepository = dataSource.getRepository(Domain);
@ -47,16 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
project: savedProjects project: savedProjects
}; };
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)); const primaryDomainsEntities = await getEntities(
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations); path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
);
const savedPrimaryDomains = await loadAndSaveData(
Domain,
dataSource,
primaryDomainsEntities,
domainPrimaryRelations
);
const domainRedirectedRelations = { const domainRedirectedRelations = {
project: savedProjects, project: savedProjects,
redirectTo: savedPrimaryDomains redirectTo: savedPrimaryDomains
}; };
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)); const redirectDomainsEntities = await getEntities(
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations); path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
);
await loadAndSaveData(
Domain,
dataSource,
redirectDomainsEntities,
domainRedirectedRelations
);
const savedDomains = await domainRepository.find(); const savedDomains = await domainRepository.find();
@ -65,16 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
organization: savedOrgs organization: savedOrgs
}; };
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)); const userOrganizationsEntities = await getEntities(
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations); path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
);
await loadAndSaveData(
UserOrganization,
dataSource,
userOrganizationsEntities,
userOrganizationRelations
);
const projectMemberRelations = { const projectMemberRelations = {
member: savedUsers, member: savedUsers,
project: savedProjects project: savedProjects
}; };
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)); const projectMembersEntities = await getEntities(
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations); path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
);
await loadAndSaveData(
ProjectMember,
dataSource,
projectMembersEntities,
projectMemberRelations
);
const deploymentRelations = { const deploymentRelations = {
project: savedProjects, project: savedProjects,
@ -82,15 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
createdBy: savedUsers createdBy: savedUsers
}; };
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH)); const deploymentsEntities = await getEntities(
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations); path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
);
await loadAndSaveData(
Deployment,
dataSource,
deploymentsEntities,
deploymentRelations
);
const environmentVariableRelations = { const environmentVariableRelations = {
project: savedProjects project: savedProjects
}; };
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)); const environmentVariablesEntities = await getEntities(
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations); path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
);
await loadAndSaveData(
EnvironmentVariable,
dataSource,
environmentVariablesEntities,
environmentVariableRelations
);
}; };
const main = async () => { const main = async () => {

View File

@ -21,12 +21,20 @@ async function main () {
const bondId = await registry.getNextBondId(registryConfig.privateKey); const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId); 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) { for await (const name of authorityNames) {
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee); await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
log('Reserved authority name:', name); 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}`); log(`Bond ${bondId} set for authority ${name}`);
} }
} }

View File

@ -14,7 +14,11 @@ const log = debug('snowball:publish-deploy-records');
async function main () { async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH); 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({ const dataSource = new DataSource({
type: 'better-sqlite3', type: 'better-sqlite3',

View File

@ -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 =

View File

@ -16,5 +16,10 @@
"plugin:react/recommended", "plugin:react/recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended" "plugin:prettier/recommended"
] ],
"settings": {
"react": {
"version": "detect"
}
}
} }

View File

@ -13,6 +13,7 @@
# misc # misc
.DS_Store .DS_Store
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local

39
packages/frontend/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,39 @@
{
// eslint extension options
"eslint.enable": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"css.customData": [".vscode/tailwind.json"],
// prettier extension setting
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.rulers": [80],
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll",
"source.organizeImports"
],
// Show in vscode "Problems" tab when there are errors
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
// Use absolute import for typescript files
"typescript.preferences.importModuleSpecifier": "non-relative",
// IntelliSense for taiwind variants
"tailwindCSS.experimental.classRegex": [
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

View File

@ -3,8 +3,17 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fontsource/inter": "^5.0.16",
"@material-tailwind/react": "^2.1.7", "@material-tailwind/react": "^2.1.7",
"@tanstack/react-query": "^5.22.2", "@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-switch": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.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/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
@ -16,13 +25,15 @@
"@web3modal/wagmi": "^4.0.5", "@web3modal/wagmi": "^4.0.5",
"assert": "^2.1.0", "assert": "^2.1.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"date-fns": "^3.0.1", "clsx": "^2.1.0",
"date-fns": "^3.3.1",
"downshift": "^8.2.3", "downshift": "^8.2.3",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"gql-client": "^1.0.0", "gql-client": "^1.0.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"octokit": "^3.1.2", "octokit": "^3.1.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-calendar": "^4.8.0",
"react-code-blocks": "^0.1.6", "react-code-blocks": "^0.1.6",
"react-day-picker": "^8.9.1", "react-day-picker": "^8.9.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -34,6 +45,7 @@
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-timer-hook": "^3.0.7", "react-timer-hook": "^3.0.7",
"siwe": "^2.1.4", "siwe": "^2.1.4",
"tailwind-variants": "^0.2.0",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"usehooks-ts": "^2.10.0", "usehooks-ts": "^2.10.0",
"vertical-stepper-nav": "^1.0.2", "vertical-stepper-nav": "^1.0.2",
@ -78,6 +90,6 @@
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"tailwindcss": "^3.3.6" "tailwindcss": "^3.4.1"
} }
} }

View File

@ -3,18 +3,29 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta <meta name="description" content="snowball tools dashboard" />
name="description"
content="snowball tools dashboard"
/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <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
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png"> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png"> sizes="180x180"
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest"> href="%PUBLIC_URL%/apple-touch-icon.png"
<meta name="msapplication-TileColor" content="#2d89ef"> />
<meta name="theme-color" content="#ffffff"> <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" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Snowball</title> <title>Snowball</title>
</head> </head>

View File

@ -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

View File

@ -126,7 +126,8 @@ const DatePicker = ({
crossOrigin={undefined} crossOrigin={undefined}
/> />
</PopoverHandler> </PopoverHandler>
<PopoverContent> {/* TODO: Figure out what placeholder is for */}
<PopoverContent placeholder={''}>
{mode === 'single' && ( {mode === 'single' && (
<DayPicker <DayPicker
mode="single" mode="single"
@ -145,19 +146,23 @@ const DatePicker = ({
/> />
<HorizontalLine /> <HorizontalLine />
<div className="flex justify-end"> <div className="flex justify-end">
{/* TODO: Figure out what placeholder is for */}
<Button <Button
size="sm" size="sm"
className="rounded-full mr-2" className="rounded-full mr-2"
variant="outlined" variant="outlined"
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
placeholder={''}
> >
Cancel Cancel
</Button> </Button>
{/* TODO: Figure out what placeholder is for */}
<Button <Button
size="sm" size="sm"
className="rounded-full" className="rounded-full"
color="gray" color="gray"
onClick={() => handleRangeSelect()} onClick={() => handleRangeSelect()}
placeholder={''}
> >
Select Select
</Button> </Button>

View File

@ -2,11 +2,13 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom'; import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization } from 'gql-client'; import { Organization } from 'gql-client';
import { Typography, Option } from '@material-tailwind/react'; import { Option } from '@material-tailwind/react';
import { useDisconnect } from 'wagmi'; import { useDisconnect } from 'wagmi';
import { useGQLClient } from '../context/GQLClientContext'; import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect'; import AsyncSelect from './shared/AsyncSelect';
import { ChevronGrabberHorizontal, GlobeIcon } from './shared/CustomIcon';
import { Tabs } from 'components/shared/Tabs';
const Sidebar = () => { const Sidebar = () => {
const { orgSlug } = useParams(); const { orgSlug } = useParams();
@ -33,61 +35,90 @@ const Sidebar = () => {
}, [disconnect, navigate]); }, [disconnect, navigate]);
return ( return (
<div className="flex flex-col h-full p-4"> <div className="flex flex-col h-full p-4 mt-5">
<div className="grow"> <div className="grow">
<div>
<Link to={`/${orgSlug}`}> <Link to={`/${orgSlug}`}>
<h3 className="text-black text-2xl">Snowball</h3> <div className="flex items-center space-x-3 mb-10 ml-2">
</Link> <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> </div>
</Link>
<AsyncSelect <AsyncSelect
className="bg-white py-2" className="bg-white rounded-lg shadow"
value={selectedOrgSlug} value={selectedOrgSlug}
onChange={(value) => { onChange={(value) => {
setSelectedOrgSlug(value!); setSelectedOrgSlug(value!);
navigate(`/${value}`); navigate(`/${value}`);
}} }}
selected={(_, index) => ( selected={(_, index) => (
<div className="flex gap-2"> <div className="flex items-center space-x-3">
<div>^</div> <img
src="/logo.svg"
alt="Application Logo"
className="h-8 w-8 rounded-lg"
/>
<div> <div>
<span>{organizations[index!]?.name}</span> <div className="text-sm font-semibold">
<Typography>Organization</Typography> {organizations[index!]?.name}
</div>
<div className="text-xs text-gray-500">Organization</div>
</div> </div>
</div> </div>
)} )}
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
> >
{/* TODO: Show label organization and manage in option */} {/* TODO: Show label organization and manage in option */}
{organizations.map((org) => ( {organizations.map((org) => (
<Option key={org.id} value={org.slug}> <Option key={org.id} value={org.slug}>
^ {org.name} <div className="flex items-center space-x-3">
{org.slug === selectedOrgSlug && <p className="float-right">^</p>} <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> </Option>
))} ))}
</AsyncSelect> </AsyncSelect>
<div> <Tabs defaultValue="Projects" orientation="vertical" className="mt-5">
<NavLink <Tabs.List>
to={`/${orgSlug}`} {['Projects', 'Settings'].slice(0, 2).map((title, index) => (
className={({ isActive }) => (isActive ? 'text-blue-500' : '')} <NavLink to={`/${orgSlug}/${title}`} key={index}>
> <Tabs.Trigger icon={<GlobeIcon />} value={title}>
<Typography>Projects</Typography> {title}
</Tabs.Trigger>
</NavLink> </NavLink>
))}
</Tabs.List>
</Tabs>
</div> </div>
<div> <div className="grow flex flex-col justify-end mb-10">
<NavLink <Tabs defaultValue="Projects" orientation="vertical">
to={`/${orgSlug}/settings`} {/* TODO: use proper link buttons */}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')} <Tabs.List>
> <Tabs.Trigger icon={<GlobeIcon />} value="">
<Typography>Settings</Typography>
</NavLink>
</div>
</div>
<div className="grow flex flex-col justify-end">
<a className="cursor-pointer" onClick={handleLogOut}> <a className="cursor-pointer" onClick={handleLogOut}>
Log Out Log Out
</a> </a>
</Tabs.Trigger>
<Tabs.Trigger icon={<GlobeIcon />} value="">
<a className="cursor-pointer">Documentation</a> <a className="cursor-pointer">Documentation</a>
</Tabs.Trigger>
<Tabs.Trigger icon={<GlobeIcon />} value="">
<a className="cursor-pointer">Support</a> <a className="cursor-pointer">Support</a>
</Tabs.Trigger>
</Tabs.List>
</Tabs>
</div> </div>
</div> </div>
); );

View File

@ -21,11 +21,15 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
return ( return (
<div className="bg-white border border-gray-200 rounded-lg shadow"> <div className="bg-white border border-gray-200 rounded-lg shadow">
<div className="flex gap-2 p-2 items-center"> <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"> <div className="grow">
<Link to={`projects/${project.id}`}> <Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography> <Typography placeholder={''}>{project.name}</Typography>
<Typography color="gray" variant="small"> <Typography color="gray" variant="small" placeholder={''}>
{project.deployments[0]?.domain?.name ?? {project.deployments[0]?.domain?.name ??
'No Production Deployment'} 'No Production Deployment'}
</Typography> </Typography>
@ -35,25 +39,27 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
<MenuHandler> <MenuHandler>
<button>...</button> <button>...</button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList placeholder={''}>
<MenuItem>^ Project settings</MenuItem> <MenuItem placeholder={''}>^ Project settings</MenuItem>
<MenuItem className="text-red-500">^ Delete project</MenuItem> <MenuItem className="text-red-500" placeholder={''}>
^ Delete project
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</div> </div>
<div className="border-t-2 border-solid p-4 bg-gray-50"> <div className="border-t-2 border-solid p-4 bg-gray-50">
{project.deployments.length > 0 ? ( {project.deployments.length > 0 ? (
<> <>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
^ {project.deployments[0].commitMessage} ^ {project.deployments[0].commitMessage}
</Typography> </Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
{relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp; {relativeTimeMs(project.deployments[0].createdAt)} on ^&nbsp;
{project.deployments[0].branch} {project.deployments[0].branch}
</Typography> </Typography>
</> </>
) : ( ) : (
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
No Production deployment No Production deployment
</Typography> </Typography>
)} )}

View File

@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${ className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
(!inputValue || !isOpen) && 'hidden' (!inputValue || !isOpen) && 'hidden'
}`} }`}
placeholder={''}
> >
<List {...getMenuProps()}> <List {...getMenuProps()}>
{items.length ? ( {items.length ? (
<> <>
<div className="p-3"> <div className="p-3">
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
Suggestions Suggestions
</Typography> </Typography>
</div> </div>
@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
<ListItem <ListItem
selected={highlightedIndex === index || selectedItem === item} selected={highlightedIndex === index || selectedItem === item}
key={item.id} key={item.id}
placeholder={''}
{...getItemProps({ item, index })} {...getItemProps({ item, index })}
> >
<ListItemPrefix> <ListItemPrefix placeholder={''}>
<Avatar src={item.icon || '/gray.png'} variant="rounded" /> <Avatar
src={item.icon || '/gray.png'}
variant="rounded"
placeholder={''}
/>
</ListItemPrefix> </ListItemPrefix>
<div> <div>
<Typography variant="h6" color="blue-gray"> <Typography variant="h6" color="blue-gray" placeholder={''}>
{item.name} {item.name}
</Typography> </Typography>
<Typography <Typography
variant="small" variant="small"
color="gray" color="gray"
className="font-normal" className="font-normal"
placeholder={''}
> >
{item.organization.name} {item.organization.name}
</Typography> </Typography>
@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
</> </>
) : ( ) : (
<div className="p-3"> <div className="p-3">
<Typography>^ No projects matching this name</Typography> <Typography placeholder={''}>
^ No projects matching this name
</Typography>
</div> </div>
)} )}
</List> </List>

View File

@ -46,9 +46,15 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
width={1000} width={1000}
height={1000} height={1000}
> >
<Button className="rounded-full mx-2">Connect to Github</Button> {/* TODO: figure out what placeholder is for */}
<Button className="rounded-full mx-2" placeholder={''}>
Connect to Github
</Button>
</OauthPopup> </OauthPopup>
<Button className="rounded-full mx-2">Connect to Gitea</Button> {/* TODO: figure out what placeholder is for */}
<Button className="rounded-full mx-2" placeholder={''}>
Connect to Gitea
</Button>
</div> </div>
<ConnectAccountTabPanel /> <ConnectAccountTabPanel />
</div> </div>

View File

@ -5,11 +5,11 @@ import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
const ConnectAccountTabPanel = () => { const ConnectAccountTabPanel = () => {
return ( return (
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import"> <Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
<TabsHeader className="grid grid-cols-2"> <TabsHeader className="grid grid-cols-2" placeholder={''}>
<Tab className="row-span-1" value="import"> <Tab className="row-span-1" value="import" placeholder={''}>
Import a repository Import a repository
</Tab> </Tab>
<Tab className="row-span-2" value="template"> <Tab className="row-span-2" value="template" placeholder={''}>
Start with a template Start with a template
</Tab> </Tab>
</TabsHeader> </TabsHeader>

View File

@ -43,7 +43,12 @@ const Deploy = () => {
</div> </div>
</div> </div>
<div> <div>
<Button onClick={handleOpen} variant="outlined" size="sm"> <Button
onClick={handleOpen}
variant="outlined"
size="sm"
placeholder={''}
>
^ Cancel ^ Cancel
</Button> </Button>
</div> </div>
@ -55,7 +60,7 @@ const Deploy = () => {
handleConfirm={handleCancel} handleConfirm={handleCancel}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
This will halt the deployment and you will have to start the process This will halt the deployment and you will have to start the process
from scratch. from scratch.
</Typography> </Typography>

View File

@ -62,7 +62,12 @@ const DeployStep = ({
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll"> <div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
{processLogs.map((log, key) => { {processLogs.map((log, key) => {
return ( return (
<Typography variant="small" color="gray" key={key}> <Typography
variant="small"
color="gray"
key={key}
placeholder={''}
>
{log} {log}
</Typography> </Typography>
); );
@ -75,6 +80,7 @@ const DeployStep = ({
toast.success('Logs copied'); toast.success('Logs copied');
}} }}
color="blue" color="blue"
placeholder={''}
> >
^ Copy log ^ Copy log
</Button> </Button>

View File

@ -66,7 +66,9 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
<Spinner className="h-4 w-4" /> <Spinner className="h-4 w-4" />
) : ( ) : (
<div className="hidden group-hover:block"> <div className="hidden group-hover:block">
<IconButton size="sm">{'>'}</IconButton> <IconButton size="sm" placeholder={''}>
{'>'}
</IconButton>
</div> </div>
)} )}
</div> </div>

View File

@ -136,11 +136,12 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
) : ( ) : (
<div className="mt-4 p-6 flex items-center justify-center"> <div className="mt-4 p-6 flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<Typography>No repository found</Typography> <Typography placeholder={''}>No repository found</Typography>
<Button <Button
className="rounded-full mt-5" className="rounded-full mt-5"
size="sm" size="sm"
onClick={handleResetFilters} onClick={handleResetFilters}
placeholder={''}
> >
^ Reset filters ^ Reset filters
</Button> </Button>

View File

@ -18,11 +18,15 @@ interface TemplateCardProps {
const CardDetails = ({ template }: { template: TemplateDetails }) => { const CardDetails = ({ template }: { template: TemplateDetails }) => {
return ( return (
<div className="h-14 group bg-gray-200 border-gray-200 rounded-lg shadow p-4 flex items-center justify-between"> <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} {template.icon} {template.name}
</Typography> </Typography>
<div> <div>
<IconButton size="sm" className="rounded-full hidden group-hover:block"> <IconButton
size="sm"
className="rounded-full hidden group-hover:block"
placeholder={''}
>
{'>'} {'>'}
</IconButton> </IconButton>
</div> </div>

View File

@ -13,20 +13,29 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
return ( return (
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1"> <div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
<div className="w-8"> <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>
<div className="grow"> <div className="grow">
<Typography>{activity.commit.author?.name}</Typography> <Typography placeholder={''}>{activity.commit.author?.name}</Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
{relativeTimeISO(activity.commit.author!.date!)} ^{' '} {relativeTimeISO(activity.commit.author!.date!)} ^{' '}
{activity.branch.name} {activity.branch.name}
</Typography> </Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray" placeholder={''}>
{activity.commit.message} {activity.commit.message}
</Typography> </Typography>
</div> </div>
<div className="mr-2 self-center hidden group-hover:block"> <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> </IconButton>
</div> </div>

View File

@ -17,9 +17,9 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => { const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader>Assign Domain</DialogHeader> <DialogHeader placeholder={''}>Assign Domain</DialogHeader>
<DialogBody> <DialogBody placeholder={''}>
In order to assign a domain to your production deployments, configure it In order to assign a domain to your production deployments, configure it
in the{' '} in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */} {/* TODO: Fix selection of project settings tab on navigation to domains */}
@ -36,12 +36,13 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
theme={atomOneLight} theme={atomOneLight}
/> />
</DialogBody> </DialogBody>
<DialogFooter className="flex justify-start"> <DialogFooter className="flex justify-start" placeholder={''}>
<Button <Button
className="rounded-3xl" className="rounded-3xl"
variant="gradient" variant="gradient"
color="blue" color="blue"
onClick={handleOpen} onClick={handleOpen}
placeholder={''}
> >
<span>Okay</span> <span>Okay</span>
</Button> </Button>

View File

@ -93,10 +93,12 @@ const DeploymentDetailsCard = ({
<div className="col-span-3"> <div className="col-span-3">
<div className="flex"> <div className="flex">
{deployment.url && ( {deployment.url && (
<Typography className=" basis-3/4">{deployment.url}</Typography> <Typography className="basis-3/4" placeholder={''}>
{deployment.url}
</Typography>
)} )}
</div> </div>
<Typography color="gray"> <Typography color="gray" placeholder={''}>
{deployment.environment === Environment.Production {deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}` ? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'} : 'Preview'}
@ -111,14 +113,16 @@ const DeploymentDetailsCard = ({
/> />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Typography color="gray">^ {deployment.branch}</Typography> <Typography color="gray" placeholder={''}>
<Typography color="gray"> ^ {deployment.branch}
</Typography>
<Typography color="gray" placeholder={''}>
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} ^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage} {deployment.commitMessage}
</Typography> </Typography>
</div> </div>
<div className="col-span-2 flex items-center"> <div className="col-span-2 flex items-center">
<Typography color="gray" className="grow"> <Typography color="gray" className="grow" placeholder={''}>
^ {relativeTimeMs(deployment.createdAt)} ^{' '} ^ {relativeTimeMs(deployment.createdAt)} ^{' '}
<Tooltip content={deployment.createdBy.name}> <Tooltip content={deployment.createdBy.name}>
{formatAddress(deployment.createdBy.name ?? '')} {formatAddress(deployment.createdBy.name ?? '')}
@ -128,18 +132,22 @@ const DeploymentDetailsCard = ({
<MenuHandler> <MenuHandler>
<button className="self-start">...</button> <button className="self-start">...</button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList placeholder={''}>
<a href={deployment.url} target="_blank" rel="noreferrer"> <a href={deployment.url} target="_blank" rel="noreferrer">
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem> <MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
^ Visit
</MenuItem>
</a> </a>
<MenuItem <MenuItem
onClick={() => setAssignDomainDialog(!assignDomainDialog)} onClick={() => setAssignDomainDialog(!assignDomainDialog)}
placeholder={''}
> >
^ Assign domain ^ Assign domain
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => setChangeToProduction(!changeToProduction)} onClick={() => setChangeToProduction(!changeToProduction)}
disabled={!(deployment.environment !== Environment.Production)} disabled={!(deployment.environment !== Environment.Production)}
placeholder={''}
> >
^ Change to production ^ Change to production
</MenuItem> </MenuItem>
@ -152,6 +160,7 @@ const DeploymentDetailsCard = ({
deployment.isCurrent deployment.isCurrent
) )
} }
placeholder={''}
> >
^ Redeploy to production ^ Redeploy to production
</MenuItem> </MenuItem>
@ -162,6 +171,7 @@ const DeploymentDetailsCard = ({
deployment.environment !== Environment.Production || deployment.environment !== Environment.Production ||
!Boolean(currentDeployment) !Boolean(currentDeployment)
} }
placeholder={''}
> >
^ Rollback to this version ^ Rollback to this version
</MenuItem> </MenuItem>
@ -180,17 +190,22 @@ const DeploymentDetailsCard = ({
}} }}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Upon confirmation, this deployment will be changed to production. Upon confirmation, this deployment will be changed to production.
</Typography> </Typography>
<DeploymentDialogBodyCard deployment={deployment} /> <DeploymentDialogBodyCard deployment={deployment} />
<Typography variant="small"> <Typography variant="small" placeholder={''}>
The new deployment will be associated with these domains: The new deployment will be associated with these domains:
</Typography> </Typography>
{prodBranchDomains.length > 0 && {prodBranchDomains.length > 0 &&
prodBranchDomains.map((value) => { prodBranchDomains.map((value) => {
return ( return (
<Typography variant="small" color="blue" key={value.id}> <Typography
variant="small"
color="blue"
key={value.id}
placeholder={''}
>
^ {value.name} ^ {value.name}
</Typography> </Typography>
); );
@ -209,16 +224,16 @@ const DeploymentDetailsCard = ({
}} }}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Upon confirmation, new deployment will be created with the same Upon confirmation, new deployment will be created with the same
source code as current deployment. source code as current deployment.
</Typography> </Typography>
<DeploymentDialogBodyCard deployment={deployment} /> <DeploymentDialogBodyCard deployment={deployment} />
<Typography variant="small"> <Typography variant="small" placeholder={''}>
These domains will point to your new deployment: These domains will point to your new deployment:
</Typography> </Typography>
{deployment.domain?.name && ( {deployment.domain?.name && (
<Typography variant="small" color="blue"> <Typography variant="small" color="blue" placeholder={''}>
{deployment.domain?.name} {deployment.domain?.name}
</Typography> </Typography>
)} )}
@ -237,7 +252,7 @@ const DeploymentDetailsCard = ({
}} }}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Upon confirmation, this deployment will replace your current Upon confirmation, this deployment will replace your current
deployment deployment
</Typography> </Typography>
@ -255,10 +270,10 @@ const DeploymentDetailsCard = ({
color: 'orange', color: 'orange',
}} }}
/> />
<Typography variant="small"> <Typography variant="small" placeholder={''}>
These domains will point to your new deployment: These domains will point to your new deployment:
</Typography> </Typography>
<Typography variant="small" color="blue"> <Typography variant="small" color="blue" placeholder={''}>
^ {currentDeployment.domain?.name} ^ {currentDeployment.domain?.name}
</Typography> </Typography>
</div> </div>

View File

@ -20,7 +20,7 @@ const DeploymentDialogBodyCard = ({
deployment, deployment,
}: DeploymentDialogBodyCardProps) => { }: DeploymentDialogBodyCardProps) => {
return ( return (
<Card className="p-2 shadow-none"> <Card className="p-2 shadow-none" placeholder={''}>
{chip && ( {chip && (
<Chip <Chip
className={`w-fit normal-case font-normal`} className={`w-fit normal-case font-normal`}
@ -30,16 +30,16 @@ const DeploymentDialogBodyCard = ({
/> />
)} )}
{deployment.url && ( {deployment.url && (
<Typography variant="small" className="text-black"> <Typography variant="small" className="text-black" placeholder={''}>
{deployment.url} {deployment.url}
</Typography> </Typography>
)} )}
<Typography variant="small"> <Typography variant="small" placeholder={''}>
^ {deployment.branch} ^{' '} ^ {deployment.branch} ^{' '}
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
{deployment.commitMessage} {deployment.commitMessage}
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small" placeholder={''}>
^ {relativeTimeMs(deployment.createdAt)} ^{' '} ^ {relativeTimeMs(deployment.createdAt)} ^{' '}
{formatAddress(deployment.createdBy.name ?? '')} {formatAddress(deployment.createdBy.name ?? '')}
</Typography> </Typography>

View File

@ -77,6 +77,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)} onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
className="rounded-full" className="rounded-full"
size="sm" size="sm"
placeholder={''}
> >
X X
</IconButton> </IconButton>

View File

@ -21,7 +21,9 @@ const AddEnvironmentVariableRow = ({
return ( return (
<div className="flex gap-1 p-2"> <div className="flex gap-1 p-2">
<div> <div>
<Typography variant="small">Key</Typography> <Typography variant="small" placeholder={''}>
Key
</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
{...register(`variables.${index}.key`, { {...register(`variables.${index}.key`, {
@ -30,7 +32,9 @@ const AddEnvironmentVariableRow = ({
/> />
</div> </div>
<div> <div>
<Typography variant="small">Value</Typography> <Typography variant="small" placeholder={''}>
Value
</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
{...register(`variables.${index}.value`, { {...register(`variables.${index}.value`, {
@ -43,6 +47,7 @@ const AddEnvironmentVariableRow = ({
size="sm" size="sm"
onClick={() => onDelete()} onClick={() => onDelete()}
disabled={isDeleteDisabled} disabled={isDeleteDisabled}
placeholder={''}
> >
{'>'} {'>'}
</IconButton> </IconButton>

View File

@ -61,23 +61,26 @@ const AddMemberDialog = ({
}, []); }, []);
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between"> <DialogHeader className="flex justify-between" placeholder={''}>
<div>Add member</div> <div>Add member</div>
<Button <Button
variant="outlined" variant="outlined"
onClick={handleOpen} onClick={handleOpen}
className="mr-1 rounded-3xl" className="mr-1 rounded-3xl"
placeholder={''}
> >
X X
</Button> </Button>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(submitHandler)}> <form onSubmit={handleSubmit(submitHandler)}>
<DialogBody className="flex flex-col gap-2 p-4"> <DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
<Typography variant="small"> <Typography variant="small" placeholder={''}>
We will send an invitation link to this email address. We will send an invitation link to this email address.
</Typography> </Typography>
<Typography variant="small">Email address</Typography> <Typography variant="small" placeholder={''}>
Email address
</Typography>
<Input <Input
type="email" type="email"
crossOrigin={undefined} crossOrigin={undefined}
@ -85,8 +88,10 @@ const AddMemberDialog = ({
required: 'email field cannot be empty', required: 'email field cannot be empty',
})} })}
/> />
<Typography variant="small">Permissions</Typography> <Typography variant="small" placeholder={''}>
<Typography variant="small"> Permissions
</Typography>
<Typography variant="small" placeholder={''}>
You can change this later if required. You can change this later if required.
</Typography> </Typography>
<Checkbox <Checkbox
@ -102,8 +107,13 @@ const AddMemberDialog = ({
color="blue" color="blue"
/> />
</DialogBody> </DialogBody>
<DialogFooter className="flex justify-start"> <DialogFooter className="flex justify-start" placeholder={''}>
<Button variant="outlined" onClick={handleOpen} className="mr-1"> <Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@ -111,6 +121,7 @@ const AddMemberDialog = ({
color="blue" color="blue"
type="submit" type="submit"
disabled={!isValid} disabled={!isValid}
placeholder={''}
> >
Send invite Send invite
</Button> </Button>

View File

@ -53,20 +53,21 @@ const DeleteProjectDialog = ({
}, [client, project, handleOpen]); }, [client, project, handleOpen]);
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between"> <DialogHeader className="flex justify-between" placeholder={''}>
<div>Delete project?</div> <div>Delete project?</div>
<Button <Button
variant="outlined" variant="outlined"
onClick={handleOpen} onClick={handleOpen}
className="mr-1 rounded-3xl" className="mr-1 rounded-3xl"
placeholder={''}
> >
X X
</Button> </Button>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(deleteProjectHandler)}> <form onSubmit={handleSubmit(deleteProjectHandler)}>
<DialogBody className="flex flex-col gap-2"> <DialogBody className="flex flex-col gap-2" placeholder={''}>
<Typography variant="paragraph"> <Typography variant="paragraph" placeholder={''}>
Deleting your project is irreversible. Enter your projects Deleting your project is irreversible. Enter your projects
name&nbsp; name&nbsp;
<span className="bg-blue-100 text-blue-700">({project.name})</span> <span className="bg-blue-100 text-blue-700">({project.name})</span>
@ -80,12 +81,17 @@ const DeleteProjectDialog = ({
validate: (value) => value === project.name, validate: (value) => value === project.name,
})} })}
/> />
<Typography variant="small" color="red"> <Typography variant="small" color="red" placeholder={''}>
^ Deleting your project is irreversible. ^ Deleting your project is irreversible.
</Typography> </Typography>
</DialogBody> </DialogBody>
<DialogFooter className="flex justify-start"> <DialogFooter className="flex justify-start" placeholder={''}>
<Button variant="outlined" onClick={handleOpen} className="mr-1"> <Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@ -93,6 +99,7 @@ const DeleteProjectDialog = ({
color="red" color="red"
type="submit" type="submit"
disabled={!isValid} disabled={!isValid}
placeholder={''}
> >
Yes, Delete project Yes, Delete project
</Button> </Button>

View File

@ -1,9 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Collapse, Typography } from '@material-tailwind/react'; import { Card, Collapse, Typography } from '@material-tailwind/react';
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow'; import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
import { Environment, EnvironmentVariable } from 'gql-client';
interface DisplayEnvironmentVariablesProps { interface DisplayEnvironmentVariablesProps {
environment: Environment; environment: Environment;
@ -30,11 +29,11 @@ const DisplayEnvironmentVariables = ({
</div> </div>
<Collapse open={openCollapse}> <Collapse open={openCollapse}>
{variables.length === 0 ? ( {variables.length === 0 ? (
<Card className="bg-gray-300 flex items-center p-4"> <Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
<Typography variant="small" className="text-black"> <Typography variant="small" className="text-black" placeholder={''}>
No environment variables added yet. No environment variables added yet.
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Once you add them, theyll show up here. Once you add them, theyll show up here.
</Typography> </Typography>
</Card> </Card>

View File

@ -68,7 +68,7 @@ const DomainCard = ({
<> <>
<div className="flex justify-between py-3"> <div className="flex justify-between py-3">
<div className="flex justify-start gap-1"> <div className="flex justify-start gap-1">
<Typography variant="h6"> <Typography variant="h6" placeholder={''}>
<i>^</i> {domain.name} <i>^</i> {domain.name}
</Typography> </Typography>
<Chip <Chip
@ -97,18 +97,20 @@ const DomainCard = ({
<MenuHandler> <MenuHandler>
<button className="border-2 rounded-full w-8 h-8">...</button> <button className="border-2 rounded-full w-8 h-8">...</button>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList placeholder={''}>
<MenuItem <MenuItem
className="text-black" className="text-black"
onClick={() => { onClick={() => {
setEditDialogOpen((preVal) => !preVal); setEditDialogOpen((preVal) => !preVal);
}} }}
placeholder={''}
> >
^ Edit domain ^ Edit domain
</MenuItem> </MenuItem>
<MenuItem <MenuItem
className="text-red-500" className="text-red-500"
onClick={() => setDeleteDialogOpen((preVal) => !preVal)} onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
placeholder={''}
> >
^ Delete domain ^ Delete domain
</MenuItem> </MenuItem>
@ -127,7 +129,7 @@ const DomainCard = ({
}} }}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Once deleted, the project{' '} Once deleted, the project{' '}
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700"> <span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
{project.name} {project.name}
@ -140,15 +142,21 @@ const DomainCard = ({
</ConfirmDialog> </ConfirmDialog>
</div> </div>
<Typography variant="small">Production</Typography> <Typography variant="small" placeholder={''}>
Production
</Typography>
{domain.status === DomainStatus.Pending && ( {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 ? ( {refreshStatus === RefreshStatus.IDLE ? (
<Typography variant="small"> <Typography variant="small" placeholder={''}>
^ Add these records to your domain and refresh to check ^ Add these records to your domain and refresh to check
</Typography> </Typography>
) : refreshStatus === RefreshStatus.CHECKING ? ( ) : refreshStatus === RefreshStatus.CHECKING ? (
<Typography variant="small" className="text-blue-500"> <Typography
variant="small"
className="text-blue-500"
placeholder={''}
>
^ Checking records for {domain.name} ^ Checking records for {domain.name}
</Typography> </Typography>
) : ( ) : (

View File

@ -122,27 +122,32 @@ const EditDomainDialog = ({
}, [domain]); }, [domain]);
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between"> <DialogHeader className="flex justify-between" placeholder={''}>
<div>Edit domain</div> <div>Edit domain</div>
<Button <Button
variant="outlined" variant="outlined"
onClick={handleOpen} onClick={handleOpen}
className="mr-1 rounded-3xl" className="mr-1 rounded-3xl"
placeholder={''}
> >
X X
</Button> </Button>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(updateDomainHandler)}> <form onSubmit={handleSubmit(updateDomainHandler)}>
<DialogBody className="flex flex-col gap-2 p-4"> <DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
<Typography variant="small">Domain name</Typography> <Typography variant="small" placeholder={''}>
Domain name
</Typography>
<Input crossOrigin={undefined} {...register('name')} /> <Input crossOrigin={undefined} {...register('name')} />
<Typography variant="small">Redirect to</Typography> <Typography variant="small" placeholder={''}>
Redirect to
</Typography>
<Controller <Controller
name="redirectedTo" name="redirectedTo"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Select {...field} disabled={isDisableDropdown}> <Select {...field} disabled={isDisableDropdown} placeholder={''}>
{redirectOptions.map((option, key) => ( {redirectOptions.map((option, key) => (
<Option key={key} value={option}> <Option key={key} value={option}>
^ {option} ^ {option}
@ -154,14 +159,16 @@ const EditDomainDialog = ({
{isDisableDropdown && ( {isDisableDropdown && (
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg"> <div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
<div>^</div> <div>^</div>
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Domain {domainRedirectedFrom ? domainRedirectedFrom.name : ''} Domain {domainRedirectedFrom ? domainRedirectedFrom.name : ''}
redirects to this domain so you can not redirect this doman redirects to this domain so you can not redirect this doman
further. further.
</Typography> </Typography>
</div> </div>
)} )}
<Typography variant="small">Git branch</Typography> <Typography variant="small" placeholder={''}>
Git branch
</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
{...register('branch', { {...register('branch', {
@ -174,13 +181,22 @@ const EditDomainDialog = ({
} }
/> />
{!isValid && ( {!isValid && (
<Typography variant="small" className="text-red-500"> <Typography
variant="small"
className="text-red-500"
placeholder={''}
>
We couldn&apos;t find this branch in the connected Git repository. We couldn&apos;t find this branch in the connected Git repository.
</Typography> </Typography>
)} )}
</DialogBody> </DialogBody>
<DialogFooter className="flex justify-start"> <DialogFooter className="flex justify-start" placeholder={''}>
<Button variant="outlined" onClick={handleOpen} className="mr-1"> <Button
variant="outlined"
onClick={handleOpen}
className="mr-1"
placeholder={''}
>
Cancel Cancel
</Button> </Button>
<Button <Button
@ -188,6 +204,7 @@ const EditDomainDialog = ({
color="blue" color="blue"
type="submit" type="submit"
disabled={!isDirty} disabled={!isDirty}
placeholder={''}
> >
Save changes Save changes
</Button> </Button>

View File

@ -84,7 +84,9 @@ const EditEnvironmentVariableRow = ({
<> <>
<div className="flex gap-1 p-2"> <div className="flex gap-1 p-2">
<div> <div>
<Typography variant="small">Key</Typography> <Typography variant="small" placeholder={''}>
Key
</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
disabled={!edit} disabled={!edit}
@ -92,7 +94,9 @@ const EditEnvironmentVariableRow = ({
/> />
</div> </div>
<div> <div>
<Typography variant="small">Value</Typography> <Typography variant="small" placeholder={''}>
Value
</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
disabled={!edit} disabled={!edit}
@ -114,6 +118,7 @@ const EditEnvironmentVariableRow = ({
<IconButton <IconButton
onClick={handleSubmit(updateEnvironmentVariableHandler)} onClick={handleSubmit(updateEnvironmentVariableHandler)}
size="sm" size="sm"
placeholder={''}
> >
{'S'} {'S'}
</IconButton> </IconButton>
@ -125,6 +130,7 @@ const EditEnvironmentVariableRow = ({
reset(); reset();
setEdit((preVal) => !preVal); setEdit((preVal) => !preVal);
}} }}
placeholder={''}
> >
{'C'} {'C'}
</IconButton> </IconButton>
@ -138,6 +144,7 @@ const EditEnvironmentVariableRow = ({
onClick={() => { onClick={() => {
setEdit((preVal) => !preVal); setEdit((preVal) => !preVal);
}} }}
placeholder={''}
> >
{'E'} {'E'}
</IconButton> </IconButton>
@ -146,6 +153,7 @@ const EditEnvironmentVariableRow = ({
<IconButton <IconButton
size="sm" size="sm"
onClick={() => setDeleteDialogOpen((preVal) => !preVal)} onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
placeholder={''}
> >
{'D'} {'D'}
</IconButton> </IconButton>
@ -162,7 +170,7 @@ const EditEnvironmentVariableRow = ({
handleConfirm={removeEnvironmentVariableHandler} handleConfirm={removeEnvironmentVariableHandler}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Are you sure you want to delete the variable&nbsp; Are you sure you want to delete the variable&nbsp;
<span className="bg-blue-100">{variable.key}</span>? <span className="bg-blue-100">{variable.key}</span>?
</Typography> </Typography>

View File

@ -104,6 +104,7 @@ const MemberCard = ({
selected={(_, index) => ( selected={(_, index) => (
<span>{DROPDOWN_OPTIONS[index!]?.label}</span> <span>{DROPDOWN_OPTIONS[index!]?.label}</span>
)} )}
placeholder={''}
> >
{DROPDOWN_OPTIONS.map((permission, key) => ( {DROPDOWN_OPTIONS.map((permission, key) => (
<Option key={key} value={permission.value}> <Option key={key} value={permission.value}>
@ -132,6 +133,7 @@ const MemberCard = ({
onClick={() => { onClick={() => {
setRemoveMemberDialogOpen((prevVal) => !prevVal); setRemoveMemberDialogOpen((prevVal) => !prevVal);
}} }}
placeholder={''}
> >
D D
</IconButton> </IconButton>
@ -152,7 +154,7 @@ const MemberCard = ({
}} }}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Once removed, {formatAddress(member.name ?? '')} ( Once removed, {formatAddress(member.name ?? '')} (
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access {formatAddress(ethAddress)}@{emailDomain}) will not be able to access
this project. this project.

View File

@ -17,14 +17,19 @@ const RepoConnectedSection = ({
<div className="flex gap-4"> <div className="flex gap-4">
<div>^</div> <div>^</div>
<div className="grow"> <div className="grow">
<Typography variant="small">{linkedRepo.full_name}</Typography> <Typography variant="small" placeholder={''}>
<Typography variant="small">Connected just now</Typography> {linkedRepo.full_name}
</Typography>
<Typography variant="small" placeholder={''}>
Connected just now
</Typography>
</div> </div>
<div> <div>
<Button <Button
onClick={() => setDisconnectRepoDialogOpen(true)} onClick={() => setDisconnectRepoDialogOpen(true)}
variant="outlined" variant="outlined"
size="sm" size="sm"
placeholder={''}
> >
^ Disconnect ^ Disconnect
</Button> </Button>
@ -39,7 +44,7 @@ const RepoConnectedSection = ({
}} }}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Any data tied to your Git project may become misconfigured. Are you Any data tied to your Git project may become misconfigured. Are you
sure you want to continue? sure you want to continue?
</Typography> </Typography>

View File

@ -54,14 +54,18 @@ const SetupDomain = () => {
className="flex flex-col gap-6 w-full" className="flex flex-col gap-6 w-full"
> >
<div> <div>
<Typography variant="h5">Setup domain name</Typography> <Typography variant="h5" placeholder={''}>
<Typography variant="small"> Setup domain name
</Typography>
<Typography variant="small" placeholder={''}>
Add your domain and setup redirects Add your domain and setup redirects
</Typography> </Typography>
</div> </div>
<div className="w-auto"> <div className="w-auto">
<Typography variant="small">Domain name</Typography> <Typography variant="small" placeholder={''}>
Domain name
</Typography>
<Input <Input
type="text" type="text"
variant="outlined" variant="outlined"
@ -76,7 +80,7 @@ const SetupDomain = () => {
{isValid && ( {isValid && (
<div> <div>
<Typography>Primary domain</Typography> <Typography placeholder={''}>Primary domain</Typography>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Radio <Radio
label={domainStr} label={domainStr}
@ -108,6 +112,7 @@ const SetupDomain = () => {
className="w-fit" className="w-fit"
color={isValid ? 'blue' : 'gray'} color={isValid ? 'blue' : 'gray'}
type="submit" type="submit"
placeholder={''}
> >
<i>^</i> Next <i>^</i> Next
</Button> </Button>

View File

@ -23,6 +23,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
navigator.clipboard.writeText(webhookUrl); navigator.clipboard.writeText(webhookUrl);
toast.success('Copied to clipboard'); toast.success('Copied to clipboard');
}} }}
placeholder={''}
> >
C C
</Button> </Button>
@ -32,6 +33,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}} }}
placeholder={''}
> >
X X
</Button> </Button>
@ -48,7 +50,7 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
}} }}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small" placeholder={''}>
Are you sure you want to delete the variable{' '} Are you sure you want to delete the variable{' '}
<span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>? <span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
</Typography> </Typography>

View File

@ -9,7 +9,7 @@ const AsyncSelect = React.forwardRef((props: SelectProps, ref: any) => {
useEffect(() => setKey((preVal) => preVal + 1), [props]); useEffect(() => setKey((preVal) => preVal + 1), [props]);
return <Select key={key} ref={ref} {...props} />; return <Select key={key} ref={ref} {...props} placeholder={''} />;
}); });
AsyncSelect.displayName = 'AsyncSelect'; AsyncSelect.displayName = 'AsyncSelect';

View File

@ -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>;

View File

@ -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>
);
};

View File

@ -0,0 +1,2 @@
export * from './Avatar';
export * from './Avatar.theme';

View File

@ -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>;

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './Badge';

View File

@ -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>;

View File

@ -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 };

View File

@ -0,0 +1,2 @@
export * from './Button';
export * from './Button.theme';

View File

@ -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;
}

View File

@ -0,0 +1,49 @@
import { VariantProps, tv } from 'tailwind-variants';
export const calendarTheme = tv({
slots: {
wrapper: [
'max-w-[352px]',
'bg-surface-floating',
'shadow-calendar',
'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>;

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './Calendar';

View File

@ -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>;

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './Checkbox';

View File

@ -30,23 +30,31 @@ const ConfirmDialog = ({
color, color,
}: ConfirmDialogProp) => { }: ConfirmDialogProp) => {
return ( return (
<Dialog open={open} handler={handleOpen}> <Dialog open={open} handler={handleOpen} placeholder={''}>
<DialogHeader className="flex justify-between"> <DialogHeader className="flex justify-between" placeholder={''}>
<Typography variant="h6">{dialogTitle} </Typography> <Typography variant="h6" placeholder={''}>
{dialogTitle}{' '}
</Typography>
<Button <Button
variant="outlined" variant="outlined"
onClick={handleOpen} onClick={handleOpen}
className=" rounded-full" className=" rounded-full"
placeholder={''}
> >
X X
</Button> </Button>
</DialogHeader> </DialogHeader>
<DialogBody>{children}</DialogBody> <DialogBody placeholder={''}>{children}</DialogBody>
<DialogFooter className="flex justify-start gap-2"> <DialogFooter className="flex justify-start gap-2" placeholder={''}>
<Button variant="outlined" onClick={handleOpen}> <Button variant="outlined" onClick={handleOpen} placeholder={''}>
Cancel Cancel
</Button> </Button>
<Button variant="gradient" color={color} onClick={handleConfirm}> <Button
variant="gradient"
color={color}
onClick={handleConfirm}
placeholder={''}
>
{confirmButtonTitle} {confirmButtonTitle}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const GlobeIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M10 17.9167C14.3723 17.9167 17.9167 14.3723 17.9167 10C17.9167 5.62776 14.3723 2.08334 10 2.08334M10 17.9167C5.62776 17.9167 2.08334 14.3723 2.08334 10C2.08334 5.62776 5.62776 2.08334 10 2.08334M10 17.9167C8.15906 17.9167 6.66668 14.3723 6.66668 10C6.66668 5.62776 8.15906 2.08334 10 2.08334M10 17.9167C11.841 17.9167 13.3333 14.3723 13.3333 10C13.3333 5.62776 11.841 2.08334 10 2.08334M17.5 10H2.50001"
stroke="currentColor"
strokeLinecap="square"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const InfoRoundFilledIcon = (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 2ZM10 11C10 10.4477 10.4477 10 11 10H12C12.5523 10 13 10.4477 13 11V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12C10.4477 12 10 11.5523 10 11ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const InfoSquareIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.96786 2.5C5.52858 2.49999 5.14962 2.49997 4.83748 2.52548C4.50802 2.55239 4.18034 2.61182 3.86503 2.77249C3.39462 3.01217 3.01217 3.39462 2.77249 3.86503C2.61182 4.18034 2.55239 4.50802 2.52548 4.83748C2.49997 5.14962 2.49999 5.52857 2.5 5.96785V14.0321C2.49999 14.4714 2.49997 14.8504 2.52548 15.1625C2.55239 15.492 2.61182 15.8197 2.77249 16.135C3.01217 16.6054 3.39462 16.9878 3.86503 17.2275C4.18034 17.3882 4.50802 17.4476 4.83748 17.4745C5.14962 17.5 5.52858 17.5 5.96786 17.5H14.0321C14.4714 17.5 14.8504 17.5 15.1625 17.4745C15.492 17.4476 15.8197 17.3882 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.3882 15.8197 17.4476 15.492 17.4745 15.1625C17.5 14.8504 17.5 14.4714 17.5 14.0321V5.96786C17.5 5.52858 17.5 5.14962 17.4745 4.83748C17.4476 4.50802 17.3882 4.18034 17.2275 3.86503C16.9878 3.39462 16.6054 3.01217 16.135 2.77249C15.8197 2.61182 15.492 2.55239 15.1625 2.52548C14.8504 2.49997 14.4714 2.49999 14.0322 2.5H5.96786ZM8.33333 9.16667C8.33333 8.70643 8.70643 8.33333 9.16667 8.33333H10C10.4602 8.33333 10.8333 8.70643 10.8333 9.16667V13.3333C10.8333 13.7936 10.4602 14.1667 10 14.1667C9.53976 14.1667 9.16667 13.7936 9.16667 13.3333V10C8.70643 10 8.33333 9.6269 8.33333 9.16667ZM10 5.83333C9.53976 5.83333 9.16667 6.20643 9.16667 6.66667C9.16667 7.1269 9.53976 7.5 10 7.5C10.4602 7.5 10.8333 7.1269 10.8333 6.66667C10.8333 6.20643 10.4602 5.83333 10 5.83333Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const LoadingIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.0002 1.66669C10.4605 1.66669 10.8336 2.03978 10.8336 2.50002V5.00002C10.8336 5.46026 10.4605 5.83335 10.0002 5.83335C9.54 5.83335 9.1669 5.46026 9.1669 5.00002V2.50002C9.1669 2.03978 9.54 1.66669 10.0002 1.66669ZM15.8928 4.10746C16.2182 4.4329 16.2182 4.96054 15.8928 5.28597L14.125 7.05374C13.7996 7.37918 13.272 7.37918 12.9465 7.05374C12.6211 6.7283 12.6211 6.20067 12.9465 5.87523L14.7143 4.10746C15.0397 3.78203 15.5674 3.78203 15.8928 4.10746ZM4.10768 4.10746C4.43312 3.78203 4.96076 3.78203 5.28619 4.10746L7.05396 5.87523C7.3794 6.20067 7.3794 6.7283 7.05396 7.05374C6.72852 7.37918 6.20088 7.37918 5.87545 7.05374L4.10768 5.28597C3.78224 4.96054 3.78224 4.4329 4.10768 4.10746ZM1.66666 10.0006C1.66666 9.54035 2.03975 9.16725 2.49999 9.16725H4.99999C5.46023 9.16725 5.83332 9.54035 5.83332 10.0006C5.83332 10.4608 5.46023 10.8339 4.99999 10.8339H2.49999C2.03975 10.8339 1.66666 10.4608 1.66666 10.0006ZM14.1667 10.0006C14.1667 9.54035 14.5398 9.16725 15 9.16725H17.5C17.9602 9.16725 18.3333 9.54035 18.3333 10.0006C18.3333 10.4608 17.9602 10.8339 17.5 10.8339H15C14.5398 10.8339 14.1667 10.4608 14.1667 10.0006ZM7.05396 12.9463C7.3794 13.2717 7.3794 13.7994 7.05396 14.1248L5.28619 15.8926C4.96076 16.218 4.43312 16.218 4.10768 15.8926C3.78224 15.5671 3.78224 15.0395 4.10768 14.7141L5.87545 12.9463C6.20088 12.6209 6.72852 12.6209 7.05396 12.9463ZM12.9465 12.9463C13.272 12.6209 13.7996 12.6209 14.125 12.9463L15.8928 14.7141C16.2182 15.0395 16.2182 15.5671 15.8928 15.8926C15.5674 16.218 15.0397 16.218 14.7143 15.8926L12.9465 14.1248C12.6211 13.7994 12.6211 13.2717 12.9465 12.9463ZM10.0002 14.1667C10.4605 14.1667 10.8336 14.5398 10.8336 15V17.5C10.8336 17.9603 10.4605 18.3334 10.0002 18.3334C9.54 18.3334 9.1669 17.9603 9.1669 17.5V15C9.1669 14.5398 9.54 14.1667 10.0002 14.1667Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const PlusIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,24 @@
# 1. What icons are compatible with this component?
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
# 2. How to add a new icon?
**2.1 Sanitising the icon**
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
2. Rename the function inside the new file you duplicated too
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
4. Depending on the svg you pasted...
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
9. Done!
**2.3 Use your newly imported icon**
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const SearchIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const WarningIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,15 @@
export * from './PlusIcon';
export * from './CustomIcon';
export * from './CheckIcon';
export * from './ChevronGrabberHorizontal';
export * from './ChevronLeft';
export * from './ChevronRight';
export * from './InfoSquareIcon';
export * from './WarningIcon';
export * from './SearchIcon';
export * from './CrossIcon';
export * from './GlobeIcon';
export * from './CalendarIcon';
export * from './CheckRoundFilledIcon';
export * from './InfoRoundFilledIcon';
export * from './LoadingIcon';

View File

@ -0,0 +1,9 @@
import { VariantProps, tv } from 'tailwind-variants';
export const datePickerTheme = tv({
slots: {
input: [],
},
});
export type DatePickerTheme = VariantProps<typeof datePickerTheme>;

View File

@ -0,0 +1,100 @@
import React, { useCallback, useState } from 'react';
import { Input, InputProps } from 'components/shared/Input';
import * as Popover from '@radix-ui/react-popover';
import { datePickerTheme } from './DatePicker.theme';
import { Calendar, CalendarProps } from 'components/shared/Calendar';
import { CalendarIcon } from 'components/shared/CustomIcon';
import { Value } from 'react-calendar/dist/cjs/shared/types';
import { format } from 'date-fns';
export interface DatePickerProps
extends Omit<InputProps, 'onChange' | 'value'> {
/**
* The props for the calendar component.
*/
calendarProps?: CalendarProps;
/**
* Optional callback function that is called when the value of the input changes.
* @param {string} value - The new value of the input.
* @returns None
*/
onChange?: (value: Value) => void;
/**
* The value of the input.
*/
value?: Value;
/**
* Whether to allow the selection of a date range.
*/
selectRange?: boolean;
}
/**
* A date picker component that allows users to select a date from a calendar.
* @param {DatePickerProps} props - The props for the date picker component.
* @returns The rendered date picker component.
*/
export const DatePicker = ({
className,
calendarProps,
value,
onChange,
selectRange = false,
...props
}: DatePickerProps) => {
const { input } = datePickerTheme();
const [open, setOpen] = useState(false);
/**
* Renders the value of the date based on the current state of `props.value`.
* @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
*/
const renderValue = useCallback(() => {
if (!value) return undefined;
if (Array.isArray(value)) {
return value
.map((date) => format(date as Date, 'dd/MM/yyyy'))
.join(' - ');
}
return format(value, 'dd/MM/yyyy');
}, [value]);
/**
* Handles the selection of a date from the calendar.
*/
const handleSelect = useCallback(
(date: Value) => {
setOpen(false);
onChange?.(date);
},
[setOpen, onChange],
);
return (
<Popover.Root open={open}>
<Popover.Trigger>
<Input
{...props}
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
readOnly
placeholder="Select a date..."
value={renderValue()}
className={input({ className })}
onClick={() => setOpen(true)}
/>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content onInteractOutside={() => setOpen(false)}>
<Calendar
{...calendarProps}
selectRange={selectRange}
value={value}
onCancel={() => setOpen(false)}
onSelect={handleSelect}
/>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
};

View File

@ -0,0 +1 @@
export * from './DatePicker';

View File

@ -0,0 +1,78 @@
import { VariantProps, tv } from 'tailwind-variants';
export const inlineNotificationTheme = tv({
slots: {
wrapper: ['rounded-xl', 'flex', 'gap-2', 'items-start', 'w-full', 'border'],
content: ['flex', 'flex-col', 'gap-1'],
title: [],
description: [],
icon: ['flex', 'items-start'],
},
variants: {
variant: {
info: {
wrapper: ['border-border-info-light', 'bg-base-bg-emphasized-info'],
title: ['text-elements-on-emphasized-info'],
description: ['text-elements-on-emphasized-info'],
icon: ['text-elements-info'],
},
danger: {
wrapper: ['border-border-danger-light', 'bg-base-bg-emphasized-danger'],
title: ['text-elements-on-emphasized-danger'],
description: ['text-elements-on-emphasized-danger'],
icon: ['text-elements-danger'],
},
warning: {
wrapper: [
'border-border-warning-light',
'bg-base-bg-emphasized-warning',
],
title: ['text-elements-on-emphasized-warning'],
description: ['text-elements-on-emphasized-warning'],
icon: ['text-elements-warning'],
},
success: {
wrapper: [
'border-border-success-light',
'bg-base-bg-emphasized-success',
],
title: ['text-elements-on-emphasized-success'],
description: ['text-elements-on-emphasized-success'],
icon: ['text-elements-success'],
},
generic: {
wrapper: ['border-border-separator', 'bg-base-bg-emphasized'],
title: ['text-elements-high-em'],
description: ['text-elements-on-emphasized-info'],
icon: ['text-elements-high-em'],
},
},
size: {
sm: {
wrapper: ['px-2', 'py-2'],
title: ['leading-4', 'text-xs'],
description: ['leading-4', 'text-xs'],
icon: ['h-4', 'w-4'],
},
md: {
wrapper: ['px-3', 'py-3'],
title: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
description: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
icon: ['h-5', 'w-5'],
},
},
hasDescription: {
true: {
title: ['font-medium'],
},
},
},
defaultVariants: {
variant: 'generic',
size: 'md',
},
});
export type InlineNotificationTheme = VariantProps<
typeof inlineNotificationTheme
>;

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