mirror of
https://github.com/QWRK-ORG/qwrk-laconic-core.git
synced 2026-09-09 22:04:07 +00:00
Apps and packages lint and build (#3)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
|
||||
node_modules
|
||||
dist
|
||||
.next
|
||||
.turbo
|
||||
@@ -1,2 +1,3 @@
|
||||
@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/
|
||||
legacy-peer-deps=true
|
||||
strict-peer-dependencies=false
|
||||
@@ -0,0 +1,41 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
.next
|
||||
out
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Debug logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,76 @@
|
||||
# backend
|
||||
|
||||
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install dependencies
|
||||
|
||||
In the root of the project, run:
|
||||
|
||||
```zsh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Build backend
|
||||
|
||||
```zsh
|
||||
yarn build --ignore frontend
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
#### Local
|
||||
|
||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||
|
||||
```zsh
|
||||
cp environments/local.toml.example environments/local.toml
|
||||
```
|
||||
|
||||
#### Staging environment variables
|
||||
|
||||
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
|
||||
|
||||
#### Production environment variables
|
||||
|
||||
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
|
||||
|
||||
### Run development server
|
||||
|
||||
```zsh
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||
|
||||
```zsh
|
||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||
```
|
||||
|
||||
### Staging
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/staging-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```zsh
|
||||
echo trigger >> .gitea/workflows/triggers/production-deploy
|
||||
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||
git push
|
||||
```
|
||||
|
||||
### Deployment status
|
||||
|
||||
Dumb for now
|
||||
|
||||
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
|
||||
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
|
||||
|
||||
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"quoteStyle": "single",
|
||||
"trailingCommas": "none",
|
||||
"semicolons": "asNeeded"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist/**/*", "node_modules/**/*", ".turbo/**/*"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
|
||||
[gitHub]
|
||||
webhookUrl = ""
|
||||
[gitHub.oAuth]
|
||||
clientId = ""
|
||||
clientSecret = ""
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
privateKey = ""
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@qwrk/backend",
|
||||
"license": "UNLICENSED",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@cosmjs/stargate": "^0.33.0",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"apollo-server-core": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.18.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
"luxon": "^3.5.0",
|
||||
"nanoid": "3",
|
||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||
"octokit": "^3.1.2",
|
||||
"openpgp": "^6.0.1",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"semver": "^7.6.0",
|
||||
"siwe": "^3.0.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typeorm": "^0.3.19",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "DEBUG=snowball:* node --enable-source-maps ./dist/index.js",
|
||||
"start:dev": "DEBUG=snowball:* ts-node ./src/index.ts",
|
||||
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "pnpm clean && tsc && pnpm copy-assets",
|
||||
"format": "biome format .",
|
||||
"format:check": "biome format --check .",
|
||||
"lint": "biome check .",
|
||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
||||
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
|
||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/cookie-session": "^2.0.49",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"prettier": "^3.1.1",
|
||||
"workspace": "^0.0.1-preview.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface SessionConfig {
|
||||
secret: string
|
||||
appOriginUrl: string
|
||||
trustProxy: boolean
|
||||
domain: string
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
host: string
|
||||
port: number
|
||||
gqlPath?: string
|
||||
sessionSecret: string
|
||||
appOriginUrl: string
|
||||
isProduction: boolean
|
||||
session: SessionConfig
|
||||
}
|
||||
|
||||
export interface DatabaseConfig {
|
||||
dbPath: string
|
||||
}
|
||||
|
||||
export interface GitHubConfig {
|
||||
webhookUrl: string
|
||||
oAuth: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegistryConfig {
|
||||
restEndpoint: string
|
||||
gqlEndpoint: string
|
||||
chainId: string
|
||||
privateKey: string
|
||||
bondId: string
|
||||
fetchDeploymentRecordDelay: number
|
||||
checkAuctionStatusDelay: number
|
||||
authority: string
|
||||
fee: {
|
||||
gas: string
|
||||
fees: string
|
||||
gasPrice: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuctionConfig {
|
||||
commitFee: string
|
||||
commitsDuration: string
|
||||
revealFee: string
|
||||
revealsDuration: string
|
||||
denom: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
server: ServerConfig
|
||||
database: DatabaseConfig
|
||||
gitHub: GitHubConfig
|
||||
registryConfig: RegistryConfig
|
||||
auction: AuctionConfig
|
||||
turnkey: {
|
||||
apiBaseUrl: string
|
||||
apiPublicKey: string
|
||||
apiPrivateKey: string
|
||||
defaultOrganizationId: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import process from 'node:process'
|
||||
|
||||
export const DEFAULT_CONFIG_FILE_PATH =
|
||||
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH ||
|
||||
'apps/backend/environments/local.toml'
|
||||
|
||||
export const DEFAULT_GQL_PATH = '/graphql'
|
||||
@@ -0,0 +1,694 @@
|
||||
import assert from 'node:assert'
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { lowercase, numbers } from 'nanoid-dictionary'
|
||||
import {
|
||||
DataSource,
|
||||
type DeepPartial,
|
||||
type FindManyOptions,
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
IsNull,
|
||||
Not
|
||||
} from 'typeorm'
|
||||
|
||||
import type { DatabaseConfig } from './config'
|
||||
import { Deployer } from './entity/Deployer'
|
||||
import { Deployment, DeploymentStatus } from './entity/Deployment'
|
||||
import { Domain } from './entity/Domain'
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable'
|
||||
import { Organization } from './entity/Organization'
|
||||
import { Project } from './entity/Project'
|
||||
import { ProjectMember } from './entity/ProjectMember'
|
||||
import { User } from './entity/User'
|
||||
import { UserOrganization } from './entity/UserOrganization'
|
||||
import type { DNSRecordAttributes } from './types'
|
||||
import { getEntities, loadAndSaveData } from './utils'
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'
|
||||
|
||||
const log = debug('snowball:database')
|
||||
|
||||
const nanoid = customAlphabet(lowercase + numbers, 8)
|
||||
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource
|
||||
|
||||
constructor({ dbPath }: DatabaseConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
entities: [path.join(__dirname, '/entity/*')],
|
||||
synchronize: true,
|
||||
logging: false
|
||||
})
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.dataSource.initialize()
|
||||
log('database initialized')
|
||||
|
||||
let organizations = await this.getOrganizations({})
|
||||
|
||||
// Load an organization if none exist
|
||||
if (!organizations.length) {
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
organizations = await loadAndSaveData(Organization, this.dataSource, [
|
||||
orgEntities[0]
|
||||
])
|
||||
}
|
||||
|
||||
// Hotfix for updating old DB data
|
||||
if (organizations[0].slug === 'snowball-tools-1') {
|
||||
const [orgEntity] = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
|
||||
await this.updateOrganization(organizations[0].id, {
|
||||
slug: orgEntity.slug as string,
|
||||
name: orgEntity.name as string
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(options: FindOneOptions<User>): Promise<User | null> {
|
||||
const userRepository = this.dataSource.getRepository(User)
|
||||
const user = await userRepository.findOne(options)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||
const userRepository = this.dataSource.getRepository(User)
|
||||
const user = await userRepository.save(data)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||
const userRepository = this.dataSource.getRepository(User)
|
||||
const updateResult = await userRepository.update({ id: user.id }, data)
|
||||
assert(updateResult.affected)
|
||||
|
||||
return updateResult.affected > 0
|
||||
}
|
||||
|
||||
async getOrganizations(
|
||||
options: FindManyOptions<Organization>
|
||||
): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||
const organizations = await organizationRepository.find(options)
|
||||
|
||||
return organizations
|
||||
}
|
||||
|
||||
async getOrganization(
|
||||
options: FindOneOptions<Organization>
|
||||
): Promise<Organization | null> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||
const organization = await organizationRepository.findOne(options)
|
||||
|
||||
return organization
|
||||
}
|
||||
|
||||
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||
|
||||
const userOrgs = await organizationRepository.find({
|
||||
where: {
|
||||
userOrganizations: {
|
||||
member: {
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return userOrgs
|
||||
}
|
||||
|
||||
async addUserOrganization(
|
||||
data: DeepPartial<UserOrganization>
|
||||
): Promise<UserOrganization> {
|
||||
const userOrganizationRepository =
|
||||
this.dataSource.getRepository(UserOrganization)
|
||||
const newUserOrganization = await userOrganizationRepository.save(data)
|
||||
|
||||
return newUserOrganization
|
||||
}
|
||||
|
||||
async updateOrganization(
|
||||
organizationId: string,
|
||||
data: DeepPartial<Organization>
|
||||
): Promise<boolean> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization)
|
||||
const updateResult = await organizationRepository.update(
|
||||
{ id: organizationId },
|
||||
data
|
||||
)
|
||||
assert(updateResult.affected)
|
||||
|
||||
return updateResult.affected > 0
|
||||
}
|
||||
|
||||
async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
const projects = await projectRepository.find(options)
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
async getProjectById(projectId: string): Promise<Project | null> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
|
||||
const project = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true AND deployments.isCanonical = true'
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.where('project.id = :projectId', {
|
||||
projectId
|
||||
})
|
||||
.getOne()
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||
const allProjects = await this.getProjects({
|
||||
where: {
|
||||
auctionId: Not(IsNull())
|
||||
},
|
||||
relations: ['deployments'],
|
||||
withDeleted: true
|
||||
})
|
||||
|
||||
const projects = allProjects.filter((project) => {
|
||||
if (project.deletedAt !== null) return false
|
||||
|
||||
return project.deployments.length === 0
|
||||
})
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
async getProjectsInOrganization(
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect(
|
||||
'project.deployments',
|
||||
'deployments',
|
||||
'deployments.isCurrent = true AND deployments.isCanonical = true'
|
||||
)
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.leftJoin('project.organization', 'organization')
|
||||
.where(
|
||||
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||
{
|
||||
userId,
|
||||
organizationSlug
|
||||
}
|
||||
)
|
||||
.getMany()
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployments with specified filter
|
||||
*/
|
||||
async getDeployments(
|
||||
options: FindManyOptions<Deployment>
|
||||
): Promise<Deployment[]> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
const deployments = await deploymentRepository.find(options)
|
||||
|
||||
return deployments
|
||||
}
|
||||
|
||||
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||
return this.getDeployments({
|
||||
relations: {
|
||||
project: true,
|
||||
createdBy: true,
|
||||
deployer: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
}
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getNonCanonicalDeploymentsByProjectId(
|
||||
projectId: string
|
||||
): Promise<Deployment[]> {
|
||||
return this.getDeployments({
|
||||
relations: {
|
||||
project: true,
|
||||
createdBy: true,
|
||||
deployer: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
isCanonical: false
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getDeployment(
|
||||
options: FindOneOptions<Deployment>
|
||||
): Promise<Deployment | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
const deployment = await deploymentRepository.findOne(options)
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
const domains = await domainRepository.find(options)
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
|
||||
const id = nanoid()
|
||||
|
||||
const updatedData = {
|
||||
...data,
|
||||
id
|
||||
}
|
||||
const deployment = await deploymentRepository.save(updatedData)
|
||||
|
||||
return deployment
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId(
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||
|
||||
const projectMembers = await projectMemberRepository.find({
|
||||
relations: {
|
||||
project: true,
|
||||
member: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return projectMembers
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId(
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable)
|
||||
|
||||
const environmentVariables = await environmentVariableRepository.find({
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
...filter
|
||||
}
|
||||
})
|
||||
|
||||
return environmentVariables
|
||||
}
|
||||
|
||||
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||
|
||||
const deleteResult = await projectMemberRepository.delete({
|
||||
id: projectMemberId
|
||||
})
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async updateProjectMemberById(
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||
const updateResult = await projectMemberRepository.update(
|
||||
{ id: projectMemberId },
|
||||
data
|
||||
)
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async addProjectMember(
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||
const newProjectMember = await projectMemberRepository.save(data)
|
||||
|
||||
return newProjectMember
|
||||
}
|
||||
|
||||
async addEnvironmentVariables(
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable)
|
||||
const savedEnvironmentVariables =
|
||||
await environmentVariableRepository.save(data)
|
||||
|
||||
return savedEnvironmentVariables
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable(
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable)
|
||||
const updateResult = await environmentVariableRepository.update(
|
||||
{ id: environmentVariableId },
|
||||
data
|
||||
)
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable(
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
this.dataSource.getRepository(EnvironmentVariable)
|
||||
const deleteResult = await environmentVariableRepository.delete({
|
||||
id: environmentVariableId
|
||||
})
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember)
|
||||
|
||||
const projectMemberWithProject = await projectMemberRepository.find({
|
||||
relations: {
|
||||
project: {
|
||||
owner: true
|
||||
},
|
||||
member: true
|
||||
},
|
||||
where: {
|
||||
id: projectMemberId
|
||||
}
|
||||
})
|
||||
|
||||
if (projectMemberWithProject.length === 0) {
|
||||
throw new Error('Member does not exist')
|
||||
}
|
||||
|
||||
return projectMemberWithProject[0]
|
||||
}
|
||||
|
||||
async getProjectsBySearchText(
|
||||
userId: string,
|
||||
searchText: string
|
||||
): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
|
||||
const projects = await projectRepository
|
||||
.createQueryBuilder('project')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.leftJoin('project.projectMembers', 'projectMembers')
|
||||
.where(
|
||||
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||
{
|
||||
userId,
|
||||
searchText: `%${searchText}%`
|
||||
}
|
||||
)
|
||||
.getMany()
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
async updateDeploymentById(
|
||||
deploymentId: string,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
return this.updateDeployment({ id: deploymentId }, data)
|
||||
}
|
||||
|
||||
async updateDeployment(
|
||||
criteria: FindOptionsWhere<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
const updateResult = await deploymentRepository.update(criteria, data)
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async updateDeploymentsByProjectIds(
|
||||
projectIds: string[],
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
|
||||
const updateResult = await deploymentRepository
|
||||
.createQueryBuilder()
|
||||
.update(Deployment)
|
||||
.set(data)
|
||||
.where('projectId IN (:...projectIds)', { projectIds })
|
||||
.execute()
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
const deployment = await deploymentRepository.findOneOrFail({
|
||||
where: {
|
||||
id: deploymentId
|
||||
}
|
||||
})
|
||||
|
||||
const deleteResult = await deploymentRepository.softRemove(deployment)
|
||||
|
||||
return Boolean(deleteResult)
|
||||
}
|
||||
|
||||
async addProject(
|
||||
user: User,
|
||||
organizationId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
|
||||
// TODO: Check if organization exists
|
||||
const newProject = projectRepository.create(data)
|
||||
// TODO: Set default empty array for webhooks in TypeORM
|
||||
newProject.webhooks = []
|
||||
// TODO: Set icon according to framework
|
||||
newProject.icon = ''
|
||||
|
||||
newProject.owner = user
|
||||
|
||||
newProject.organization = Object.assign(new Organization(), {
|
||||
id: organizationId
|
||||
})
|
||||
|
||||
return projectRepository.save(newProject)
|
||||
}
|
||||
|
||||
async saveProject(project: Project): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
|
||||
return projectRepository.save(project)
|
||||
}
|
||||
|
||||
async updateProjectById(
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
const updateResult = await projectRepository.update({ id: projectId }, data)
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project)
|
||||
const project = await projectRepository.findOneOrFail({
|
||||
where: {
|
||||
id: projectId
|
||||
},
|
||||
relations: {
|
||||
projectMembers: true
|
||||
}
|
||||
})
|
||||
|
||||
const deleteResult = await projectRepository.softRemove(project)
|
||||
|
||||
return Boolean(deleteResult)
|
||||
}
|
||||
|
||||
async deleteDomainById(domainId: string): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
|
||||
const deleteResult = await domainRepository.softDelete({ id: domainId })
|
||||
|
||||
if (deleteResult.affected) {
|
||||
return deleteResult.affected > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
const newDomain = await domainRepository.save(data)
|
||||
|
||||
return newDomain
|
||||
}
|
||||
|
||||
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
const domain = await domainRepository.findOne(options)
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
async updateDomainById(
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
const updateResult = await domainRepository.update({ id: domainId }, data)
|
||||
|
||||
return Boolean(updateResult.affected)
|
||||
}
|
||||
|
||||
async getDomainsByProjectId(
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
|
||||
const domains = await domainRepository.find({
|
||||
relations: {
|
||||
redirectTo: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
...filter
|
||||
}
|
||||
})
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
async getOldestDomainByProjectId(projectId: string): Promise<Domain | null> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain)
|
||||
|
||||
const domain = await domainRepository.findOne({
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
}
|
||||
},
|
||||
order: {
|
||||
createdAt: 'ASC'
|
||||
}
|
||||
})
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
async getLatestDNSRecordByProjectId(
|
||||
projectId: string
|
||||
): Promise<DNSRecordAttributes | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment)
|
||||
|
||||
const deployment = await deploymentRepository.findOne({
|
||||
where: {
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
status: DeploymentStatus.Ready
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
})
|
||||
|
||||
if (deployment === null) {
|
||||
throw new Error(`No deployment found for project ${projectId}`)
|
||||
}
|
||||
|
||||
return deployment.dnsRecordData
|
||||
}
|
||||
|
||||
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||
const newDomain = await deployerRepository.save(data)
|
||||
|
||||
return newDomain
|
||||
}
|
||||
|
||||
async getDeployers(): Promise<Deployer[]> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||
const deployers = await deployerRepository.find()
|
||||
return deployers
|
||||
}
|
||||
|
||||
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||
const deployerRepository = this.dataSource.getRepository(Deployer)
|
||||
const deployer = await deployerRepository.findOne({
|
||||
where: { deployerLrn }
|
||||
})
|
||||
|
||||
return deployer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm'
|
||||
import { Project } from './Project'
|
||||
|
||||
@Entity()
|
||||
export class Deployer {
|
||||
@PrimaryColumn('varchar')
|
||||
deployerLrn!: string
|
||||
|
||||
@Column('varchar')
|
||||
deployerId!: string
|
||||
|
||||
@Column('varchar')
|
||||
deployerApiUrl!: string
|
||||
|
||||
@Column('varchar')
|
||||
baseDomain!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
publicKey!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
minimumPayment!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
paymentAddress!: string | null
|
||||
|
||||
@ManyToMany(
|
||||
() => Project,
|
||||
(project) => project.deployers
|
||||
)
|
||||
projects!: Project[]
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import type {
|
||||
AppDeploymentRecordAttributes,
|
||||
AppDeploymentRemovalRecordAttributes,
|
||||
DNSRecordAttributes
|
||||
} from '../types'
|
||||
import { Deployer } from './Deployer'
|
||||
import { Project } from './Project'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development'
|
||||
}
|
||||
|
||||
export enum DeploymentStatus {
|
||||
Building = 'Building',
|
||||
Ready = 'Ready',
|
||||
Error = 'Error',
|
||||
Deleting = 'Deleting'
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRequest {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
application: string
|
||||
lrn?: string
|
||||
auction?: string
|
||||
config: string
|
||||
meta: string
|
||||
payment?: string
|
||||
dns?: string
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string
|
||||
version: string
|
||||
deployment: string
|
||||
auction?: string
|
||||
payment?: string
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string
|
||||
version: string
|
||||
name: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
author?: string
|
||||
repository?: string[]
|
||||
app_version?: string
|
||||
repository_ref: string
|
||||
app_type: string
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Deployment {
|
||||
// TODO: set custom generated id
|
||||
@PrimaryColumn('varchar')
|
||||
id!: string
|
||||
|
||||
@Column()
|
||||
projectId!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column('varchar')
|
||||
branch!: string
|
||||
|
||||
@Column('varchar')
|
||||
commitHash!: string
|
||||
|
||||
@Column('varchar')
|
||||
commitMessage!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
url!: string | null
|
||||
|
||||
@Column('varchar')
|
||||
applicationRecordId!: string
|
||||
|
||||
@Column('simple-json')
|
||||
applicationRecordData!: ApplicationRecord
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRequestId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRecordId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
dnsRecordData!: DNSRecordAttributes | null
|
||||
|
||||
@ManyToOne(() => Deployer)
|
||||
@JoinColumn({ name: 'deployerLrn' })
|
||||
deployer!: Deployer
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
environment!: Environment
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isCurrent!: boolean
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isCanonical!: boolean
|
||||
|
||||
@Column({
|
||||
enum: DeploymentStatus
|
||||
})
|
||||
status!: DeploymentStatus
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'createdBy' })
|
||||
createdBy!: User
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
|
||||
export enum Status {
|
||||
Live = 'Live',
|
||||
Pending = 'Pending'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Domain {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Column('varchar')
|
||||
projectId!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column('varchar', { length: 255, default: 'main' })
|
||||
branch!: string
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string
|
||||
|
||||
@Column('string', { nullable: true })
|
||||
redirectToId!: string | null
|
||||
|
||||
@ManyToOne(() => Domain)
|
||||
@JoinColumn({ name: 'redirectToId' })
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
redirectTo!: Domain | null
|
||||
|
||||
@Column({
|
||||
enum: Status,
|
||||
default: Status.Pending
|
||||
})
|
||||
status!: Status
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
|
||||
enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class EnvironmentVariable {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
environment!: Environment
|
||||
|
||||
@Column('varchar')
|
||||
key!: string
|
||||
|
||||
@Column('varchar')
|
||||
value!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
import { UserOrganization } from './UserOrganization'
|
||||
|
||||
@Entity()
|
||||
@Unique(['slug'])
|
||||
export class Organization {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string
|
||||
|
||||
@Column('varchar')
|
||||
slug!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(userOrganization) => userOrganization.organization,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Deployer } from './Deployer'
|
||||
import { Deployment } from './Deployment'
|
||||
import { Organization } from './Organization'
|
||||
import { ProjectMember } from './ProjectMember'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'ownerId' })
|
||||
owner!: User
|
||||
|
||||
@Column({ nullable: false })
|
||||
ownerId!: string
|
||||
|
||||
@ManyToOne(() => Organization, { nullable: true })
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: Organization | null
|
||||
|
||||
@Column('varchar')
|
||||
organizationId!: string
|
||||
|
||||
@Column('varchar')
|
||||
name!: string
|
||||
|
||||
@Column('varchar')
|
||||
repository!: string
|
||||
|
||||
@Column('varchar', { length: 255, default: 'main' })
|
||||
prodBranch!: string
|
||||
|
||||
@Column('text', { default: '' })
|
||||
description!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
auctionId!: string | null
|
||||
|
||||
// Tx hash for sending coins from snowball to deployer
|
||||
@Column('varchar', { nullable: true })
|
||||
txHash!: string | null
|
||||
|
||||
@ManyToMany(
|
||||
() => Deployer,
|
||||
(deployer) => deployer.projects
|
||||
)
|
||||
@JoinTable()
|
||||
deployers!: Deployer[]
|
||||
|
||||
@Column('boolean', { default: false, nullable: true })
|
||||
fundsReleased!: boolean
|
||||
|
||||
// TODO: Compute template & framework in import repository
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
framework!: string | null
|
||||
|
||||
// Address of the user who created the project i.e. requested deployments
|
||||
@Column('varchar')
|
||||
paymentAddress!: string
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
webhooks!: string[]
|
||||
|
||||
@Column('varchar')
|
||||
icon!: string
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
|
||||
@OneToMany(
|
||||
() => Deployment,
|
||||
(deployment) => deployment.project
|
||||
)
|
||||
deployments!: Deployment[]
|
||||
|
||||
@OneToMany(
|
||||
() => ProjectMember,
|
||||
(projectMember) => projectMember.project,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
projectMembers!: ProjectMember[]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Project } from './Project'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@Unique(['project', 'member'])
|
||||
export class ProjectMember {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(
|
||||
() => User,
|
||||
(user) => user.projectMembers
|
||||
)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User
|
||||
|
||||
@ManyToOne(
|
||||
() => Project,
|
||||
(project) => project.projectMembers
|
||||
)
|
||||
@JoinColumn({ name: 'projectId' })
|
||||
project!: Project
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
permissions!: Permission[]
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isPending!: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique
|
||||
} from 'typeorm'
|
||||
|
||||
import { ProjectMember } from './ProjectMember'
|
||||
import { UserOrganization } from './UserOrganization'
|
||||
|
||||
@Entity()
|
||||
@Unique(['email'])
|
||||
@Unique(['ethAddress'])
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
// TODO: Set ethAddress as ID
|
||||
@Column()
|
||||
ethAddress!: string
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true })
|
||||
name!: string | null
|
||||
|
||||
@Column()
|
||||
email!: string
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
gitHubToken!: string | null
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
isVerified!: boolean
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@Column()
|
||||
subOrgId!: string
|
||||
|
||||
@Column()
|
||||
turnkeyWalletId!: string
|
||||
|
||||
@OneToMany(
|
||||
() => ProjectMember,
|
||||
(projectMember) => projectMember.project,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
projectMembers!: ProjectMember[]
|
||||
|
||||
@OneToMany(
|
||||
() => UserOrganization,
|
||||
(UserOrganization) => UserOrganization.member,
|
||||
{
|
||||
cascade: ['soft-remove']
|
||||
}
|
||||
)
|
||||
userOrganizations!: UserOrganization[]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn
|
||||
} from 'typeorm'
|
||||
|
||||
import { Organization } from './Organization'
|
||||
import { User } from './User'
|
||||
|
||||
export enum Role {
|
||||
Owner = 'Owner',
|
||||
Maintainer = 'Maintainer',
|
||||
Reader = 'Reader'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class UserOrganization {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
member!: User
|
||||
|
||||
@ManyToOne(() => Organization)
|
||||
@JoinColumn({ name: 'organizationId' })
|
||||
organization!: Organization
|
||||
|
||||
@Column({
|
||||
enum: Role
|
||||
})
|
||||
role!: Role
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'express-async-errors'
|
||||
import 'reflect-metadata'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app'
|
||||
|
||||
import { Database } from './database'
|
||||
import { Registry } from './registry'
|
||||
import { createResolvers } from './resolvers'
|
||||
import { createAndStartServer } from './server'
|
||||
import { Service } from './service'
|
||||
import { getConfig } from './utils'
|
||||
|
||||
const log = debug('snowball:server')
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app'
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
const { server, database, gitHub, registryConfig } = await getConfig()
|
||||
|
||||
const app = new OAuthApp({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
clientId: gitHub.oAuth.clientId,
|
||||
clientSecret: gitHub.oAuth.clientSecret
|
||||
})
|
||||
|
||||
const db = new Database(database)
|
||||
await db.init()
|
||||
|
||||
const registry = new Registry(registryConfig)
|
||||
const service = new Service(
|
||||
{ gitHubConfig: gitHub, registryConfig },
|
||||
db,
|
||||
app,
|
||||
registry
|
||||
)
|
||||
|
||||
const typeDefs = fs
|
||||
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||
.toString()
|
||||
const resolvers = await createResolvers(service)
|
||||
|
||||
await createAndStartServer(server, typeDefs, resolvers, service)
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
log('Starting server...')
|
||||
})
|
||||
.catch((err) => {
|
||||
log(err)
|
||||
})
|
||||
@@ -0,0 +1,624 @@
|
||||
import assert from 'node:assert'
|
||||
import debug from 'debug'
|
||||
import { DateTime } from 'luxon'
|
||||
import type { Octokit } from 'octokit'
|
||||
import * as openpgp from 'openpgp'
|
||||
import { inc as semverInc } from 'semver'
|
||||
import type { DeepPartial } from 'typeorm'
|
||||
|
||||
import {
|
||||
Account,
|
||||
DEFAULT_GAS_ESTIMATION_MULTIPLIER,
|
||||
Registry as LaconicRegistry,
|
||||
getGasPrice,
|
||||
parseGasAndFees
|
||||
} from '@cerc-io/registry-sdk'
|
||||
import type { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate'
|
||||
|
||||
import type { RegistryConfig } from './config'
|
||||
import type {
|
||||
ApplicationDeploymentRemovalRequest,
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationRecord,
|
||||
Deployment
|
||||
} from './entity/Deployment'
|
||||
import type {
|
||||
AppDeploymentRecord,
|
||||
AppDeploymentRemovalRecord,
|
||||
AuctionParams,
|
||||
DeployerRecord,
|
||||
RegistryRecord
|
||||
} from './types'
|
||||
import {
|
||||
getConfig,
|
||||
getRepoDetails,
|
||||
registryTransactionWithRetry,
|
||||
sleep
|
||||
} from './utils'
|
||||
|
||||
const log = debug('snowball:registry')
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord'
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction'
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE =
|
||||
'ApplicationDeploymentRemovalRequest'
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'
|
||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'
|
||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||
const SLEEP_DURATION = 1000
|
||||
|
||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry
|
||||
private registryConfig: RegistryConfig
|
||||
|
||||
constructor(registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice)
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
)
|
||||
}
|
||||
|
||||
async createApplicationRecord({
|
||||
octokit,
|
||||
repository,
|
||||
commitHash,
|
||||
appType
|
||||
}: {
|
||||
octokit: Octokit
|
||||
repository: string
|
||||
commitHash: string
|
||||
appType: string
|
||||
}): Promise<{
|
||||
applicationRecordId: string
|
||||
applicationRecordData: ApplicationRecord
|
||||
}> {
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(
|
||||
octokit,
|
||||
repository,
|
||||
commitHash
|
||||
)
|
||||
// Use registry-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_RECORD_TYPE,
|
||||
name: packageJSON.name
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
// Get next version of record
|
||||
const bondRecords = records.filter(
|
||||
(record: any) => record.bondId === this.registryConfig.bondId
|
||||
)
|
||||
const [latestBondRecord] = bondRecords.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
)
|
||||
const nextVersion = semverInc(
|
||||
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||
'patch'
|
||||
)
|
||||
|
||||
assert(nextVersion, 'Application record version not valid')
|
||||
|
||||
// Create record of type ApplicationRecord and publish
|
||||
const applicationRecord = {
|
||||
type: APP_RECORD_TYPE,
|
||||
version: nextVersion,
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: repo,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
...(packageJSON.author && {
|
||||
author:
|
||||
typeof packageJSON.author === 'object'
|
||||
? JSON.stringify(packageJSON.author)
|
||||
: packageJSON.author
|
||||
}),
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
}
|
||||
|
||||
const result = await this.publishRecord(applicationRecord)
|
||||
|
||||
log(`Published application record ${result.id}`)
|
||||
log('Application record data:', applicationRecord)
|
||||
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(repo)
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`)
|
||||
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
applicationRecordId: result.id,
|
||||
applicationRecordData: applicationRecord
|
||||
}
|
||||
}
|
||||
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<{
|
||||
applicationDeploymentAuctionId: string
|
||||
}> {
|
||||
assert(data.project?.repository, 'Project repository not found')
|
||||
|
||||
await this.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!
|
||||
})
|
||||
|
||||
const lrn = this.getLrn(appName)
|
||||
const config = await getConfig()
|
||||
const auctionConfig = config.auction
|
||||
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
const auctionResult = await registryTransactionWithRetry(() =>
|
||||
this.registry.createProviderAuction(
|
||||
{
|
||||
commitFee: auctionConfig.commitFee,
|
||||
commitsDuration: auctionConfig.commitsDuration,
|
||||
revealFee: auctionConfig.revealFee,
|
||||
revealsDuration: auctionConfig.revealsDuration,
|
||||
denom: auctionConfig.denom,
|
||||
maxPrice: auctionParams.maxPrice,
|
||||
numProviders: auctionParams.numProviders
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
if (!auctionResult.auction) {
|
||||
throw new Error('Error creating auction')
|
||||
}
|
||||
|
||||
// Create record of type applicationDeploymentAuction and publish
|
||||
const applicationDeploymentAuction = {
|
||||
application: lrn,
|
||||
auction: auctionResult.auction.id,
|
||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE
|
||||
}
|
||||
|
||||
const result = await this.publishRecord(applicationDeploymentAuction)
|
||||
|
||||
log(`Application deployment auction created: ${auctionResult.auction.id}`)
|
||||
log(`Application deployment auction record published: ${result.id}`)
|
||||
log('Application deployment auction data:', applicationDeploymentAuction)
|
||||
|
||||
return {
|
||||
applicationDeploymentAuctionId: auctionResult.auction.id
|
||||
}
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest(data: {
|
||||
deployment: Deployment
|
||||
appName: string
|
||||
repository: string
|
||||
auctionId?: string | null
|
||||
lrn: string
|
||||
apiUrl: string
|
||||
environmentVariables: { [key: string]: string }
|
||||
dns: string
|
||||
requesterAddress: string
|
||||
publicKey: string
|
||||
payment?: string | null
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
||||
}> {
|
||||
const lrn = this.getLrn(data.appName)
|
||||
const records = await this.registry.resolveNames([lrn])
|
||||
const applicationRecord = records[0]
|
||||
|
||||
if (!applicationRecord) {
|
||||
throw new Error(`No record found for ${lrn}`)
|
||||
}
|
||||
|
||||
let hash: string | undefined
|
||||
if (Object.keys(data.environmentVariables).length !== 0) {
|
||||
hash = await this.generateConfigHash(
|
||||
data.environmentVariables,
|
||||
data.requesterAddress,
|
||||
data.publicKey,
|
||||
data.apiUrl
|
||||
)
|
||||
}
|
||||
|
||||
// Create record of type ApplicationDeploymentRequest and publish
|
||||
const applicationDeploymentRequest = {
|
||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
config: JSON.stringify(hash ? { ref: hash } : {}),
|
||||
meta: JSON.stringify({
|
||||
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
}),
|
||||
deployer: data.lrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment })
|
||||
}
|
||||
|
||||
await sleep(SLEEP_DURATION)
|
||||
|
||||
const result = await this.publishRecord(applicationDeploymentRequest)
|
||||
|
||||
log(`Application deployment request record published: ${result.id}`)
|
||||
log('Application deployment request data:', applicationDeploymentRequest)
|
||||
|
||||
return {
|
||||
applicationDeploymentRequestId: result.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
}
|
||||
}
|
||||
|
||||
async getAuctionWinningDeployerRecords(
|
||||
auctionId: string
|
||||
): Promise<DeployerRecord[]> {
|
||||
const records = await this.registry.getAuctionsByIds([auctionId])
|
||||
const auctionResult = records[0]
|
||||
|
||||
const deployerRecords = []
|
||||
const { winnerAddresses } = auctionResult
|
||||
|
||||
for (const auctionWinner of winnerAddresses) {
|
||||
const records = await this.getDeployerRecordsByFilter({
|
||||
paymentAddress: auctionWinner
|
||||
})
|
||||
|
||||
const newRecords = records.filter((record) => {
|
||||
return record.names !== null && record.names.length > 0
|
||||
})
|
||||
|
||||
for (const record of newRecords) {
|
||||
if (record.id) {
|
||||
deployerRecords.push(record)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deployerRecords
|
||||
}
|
||||
|
||||
async releaseDeployerFunds(auctionId: string): Promise<any> {
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
const auction = await registryTransactionWithRetry(() =>
|
||||
this.registry.releaseFunds(
|
||||
{
|
||||
auctionId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
return auction
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords for deployments
|
||||
*/
|
||||
async getDeploymentRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
// Filter records with ApplicationDeploymentRequestId ID
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationDeploymentRequestId ===
|
||||
record.attributes.request
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch WebappDeployer Records by filter
|
||||
*/
|
||||
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<
|
||||
DeployerRecord[]
|
||||
> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
||||
...filter
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRecords by filter
|
||||
*/
|
||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<
|
||||
AppDeploymentRecord[]
|
||||
> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||
...filter
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
*/
|
||||
async getDeploymentRemovalRecords(
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRemovalRecord[]> {
|
||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||
const records = await this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs
|
||||
return records.filter((record: AppDeploymentRemovalRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationDeploymentRemovalRequestId ===
|
||||
record.attributes.request &&
|
||||
deployment.applicationDeploymentRecordId ===
|
||||
record.attributes.deployment
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch record by Id
|
||||
*/
|
||||
async getRecordById(id: string): Promise<RegistryRecord | null> {
|
||||
const [record] = await this.registry.getRecordsByIds([id])
|
||||
return record ?? null
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest(data: {
|
||||
deploymentId: string
|
||||
deployerLrn: string
|
||||
auctionId?: string | null
|
||||
payment?: string | null
|
||||
}): Promise<{
|
||||
applicationDeploymentRemovalRequestId: string
|
||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest
|
||||
}> {
|
||||
const applicationDeploymentRemovalRequest = {
|
||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
deployment: data.deploymentId,
|
||||
deployer: data.deployerLrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment })
|
||||
}
|
||||
|
||||
const result = await this.publishRecord(applicationDeploymentRemovalRequest)
|
||||
|
||||
log(`Application deployment removal request record published: ${result.id}`)
|
||||
log(
|
||||
'Application deployment removal request data:',
|
||||
applicationDeploymentRemovalRequest
|
||||
)
|
||||
|
||||
return {
|
||||
applicationDeploymentRemovalRequestId: result.id,
|
||||
applicationDeploymentRemovalRequestData:
|
||||
applicationDeploymentRemovalRequest
|
||||
}
|
||||
}
|
||||
|
||||
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||
if (auctionIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const auctions = await this.registry.getAuctionsByIds(auctionIds)
|
||||
|
||||
const completedAuctions = auctions
|
||||
.filter(
|
||||
(auction: { id: string; status: string }) =>
|
||||
auction.status === 'completed'
|
||||
)
|
||||
.map((auction: { id: string; status: string }) => auction.id)
|
||||
|
||||
return completedAuctions
|
||||
}
|
||||
|
||||
async publishRecord(recordData: any): Promise<any> {
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: recordData,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async getRecordsByName(name: string): Promise<any> {
|
||||
return this.registry.resolveNames([name])
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<any> {
|
||||
return this.registry.getAuctionsByIds([auctionId])
|
||||
}
|
||||
|
||||
async sendTokensToAccount(
|
||||
receiverAddress: string,
|
||||
amount: string
|
||||
): Promise<DeliverTxResponse> {
|
||||
const fee = parseGasAndFees(
|
||||
this.registryConfig.fee.gas,
|
||||
this.registryConfig.fee.fees
|
||||
)
|
||||
const account = await this.getAccount()
|
||||
const laconicClient = await this.registry.getLaconicClient(account)
|
||||
const txResponse: DeliverTxResponse = await registryTransactionWithRetry(
|
||||
() =>
|
||||
laconicClient.sendTokens(
|
||||
account.address,
|
||||
receiverAddress,
|
||||
[
|
||||
{
|
||||
denom: 'alnt',
|
||||
amount
|
||||
}
|
||||
],
|
||||
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER
|
||||
)
|
||||
)
|
||||
|
||||
return txResponse
|
||||
}
|
||||
|
||||
async getAccount(): Promise<Account> {
|
||||
const account = new Account(
|
||||
Buffer.from(this.registryConfig.privateKey, 'hex')
|
||||
)
|
||||
await account.init()
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||
const account = await this.getAccount()
|
||||
const laconicClient = await this.registry.getLaconicClient(account)
|
||||
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash)
|
||||
|
||||
return txResponse
|
||||
}
|
||||
|
||||
getLrn(appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist")
|
||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`
|
||||
}
|
||||
|
||||
async generateConfigHash(
|
||||
environmentVariables: { [key: string]: string },
|
||||
requesterAddress: string,
|
||||
pubKey: string,
|
||||
url: string
|
||||
): Promise<string> {
|
||||
// Config to be encrypted
|
||||
const config = {
|
||||
authorized: [requesterAddress],
|
||||
config: { env: environmentVariables }
|
||||
}
|
||||
|
||||
// Serialize the config
|
||||
const serialized = JSON.stringify(config, null, 2)
|
||||
|
||||
const armoredKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n${pubKey}\n\n-----END PGP PUBLIC KEY BLOCK-----`
|
||||
const publicKey = await openpgp.readKey({ armoredKey })
|
||||
|
||||
// Encrypt the config
|
||||
const encrypted = await openpgp.encrypt({
|
||||
message: await openpgp.createMessage({ text: serialized }),
|
||||
encryptionKeys: publicKey,
|
||||
format: 'binary'
|
||||
})
|
||||
|
||||
// Get the hash after uploading encrypted config
|
||||
const response = await fetch(`${url}/upload/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
body: encrypted
|
||||
})
|
||||
|
||||
const configHash = await response.json()
|
||||
|
||||
return configHash.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import debug from 'debug'
|
||||
import type { DeepPartial, FindOptionsWhere } from 'typeorm'
|
||||
|
||||
import type { Domain } from './entity/Domain'
|
||||
import type { EnvironmentVariable } from './entity/EnvironmentVariable'
|
||||
import type { Project } from './entity/Project'
|
||||
import type { Permission } from './entity/ProjectMember'
|
||||
import type { Service } from './service'
|
||||
import type {
|
||||
AddProjectFromTemplateInput,
|
||||
AuctionParams,
|
||||
EnvironmentVariables
|
||||
} from './types'
|
||||
|
||||
const log = debug('snowball:resolver')
|
||||
|
||||
export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return {
|
||||
Query: {
|
||||
// TODO: add custom type for context
|
||||
user: (_: any, __: any, context: any) => {
|
||||
return context.user
|
||||
},
|
||||
|
||||
organizations: async (_: any, __: any, context: any) => {
|
||||
return service.getOrganizationsByUserId(context.user)
|
||||
},
|
||||
|
||||
project: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.getProjectById(context.user, projectId)
|
||||
},
|
||||
|
||||
projectsInOrganization: async (
|
||||
_: any,
|
||||
{ organizationSlug }: { organizationSlug: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.getProjectsInOrganization(context.user, organizationSlug)
|
||||
},
|
||||
|
||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getNonCanonicalDeploymentsByProjectId(projectId)
|
||||
},
|
||||
|
||||
environmentVariables: async (
|
||||
_: any,
|
||||
{ projectId }: { projectId: string }
|
||||
) => {
|
||||
return service.getEnvironmentVariablesByProjectId(projectId)
|
||||
},
|
||||
|
||||
projectMembers: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getProjectMembersByProjectId(projectId)
|
||||
},
|
||||
|
||||
searchProjects: async (
|
||||
_: any,
|
||||
{ searchText }: { searchText: string },
|
||||
context: any
|
||||
) => {
|
||||
return service.searchProjects(context.user, searchText)
|
||||
},
|
||||
|
||||
domains: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
filter
|
||||
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
||||
) => {
|
||||
return service.getDomainsByProjectId(projectId, filter)
|
||||
},
|
||||
|
||||
getAuctionData: async (_: any, { auctionId }: { auctionId: string }) => {
|
||||
return service.getAuctionData(auctionId)
|
||||
},
|
||||
|
||||
deployers: async (_: any, __: any) => {
|
||||
return service.getDeployers()
|
||||
},
|
||||
|
||||
address: async (_: any, __: any) => {
|
||||
return service.getAddress()
|
||||
},
|
||||
|
||||
verifyTx: async (
|
||||
_: any,
|
||||
{
|
||||
txHash,
|
||||
amount,
|
||||
senderAddress
|
||||
}: { txHash: string; amount: string; senderAddress: string }
|
||||
) => {
|
||||
return service.verifyTx(txHash, amount, senderAddress)
|
||||
},
|
||||
|
||||
latestDNSRecord: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getLatestDNSRecordByProjectId(projectId)
|
||||
}
|
||||
},
|
||||
|
||||
// TODO: Return error in GQL response
|
||||
Mutation: {
|
||||
removeProjectMember: async (
|
||||
_: any,
|
||||
{ projectMemberId }: { projectMemberId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.removeProjectMember(
|
||||
context.user,
|
||||
projectMemberId
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
updateProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectMemberId,
|
||||
data
|
||||
}: {
|
||||
projectMemberId: string
|
||||
data: {
|
||||
permissions: Permission[]
|
||||
}
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProjectMember(projectMemberId, data)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
addProjectMember: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string
|
||||
data: {
|
||||
email: string
|
||||
permissions: Permission[]
|
||||
}
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addProjectMember(projectId, data))
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
addEnvironmentVariables: async (
|
||||
_: any,
|
||||
{
|
||||
projectId,
|
||||
data
|
||||
}: {
|
||||
projectId: string
|
||||
data: { environments: string[]; key: string; value: string }[]
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addEnvironmentVariables(projectId, data))
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
updateEnvironmentVariable: async (
|
||||
_: any,
|
||||
{
|
||||
environmentVariableId,
|
||||
data
|
||||
}: {
|
||||
environmentVariableId: string
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
}
|
||||
) => {
|
||||
try {
|
||||
return await service.updateEnvironmentVariable(
|
||||
environmentVariableId,
|
||||
data
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
removeEnvironmentVariable: async (
|
||||
_: any,
|
||||
{ environmentVariableId }: { environmentVariableId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.removeEnvironmentVariable(environmentVariableId)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
updateDeploymentToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.updateDeploymentToProd(context.user, deploymentId)
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
addProjectFromTemplate: async (
|
||||
_: any,
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: string
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: EnvironmentVariables[]
|
||||
},
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.addProjectFromTemplate(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
addProject: async (
|
||||
_: any,
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string
|
||||
data: DeepPartial<Project>
|
||||
lrn: string
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: EnvironmentVariables[]
|
||||
},
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
updateProject: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateProject(projectId, data)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
redeployToProd: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return Boolean(
|
||||
await service.redeployToProd(context.user, deploymentId)
|
||||
)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
|
||||
try {
|
||||
return await service.deleteProject(projectId)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
deleteDomain: async (_: any, { domainId }: { domainId: string }) => {
|
||||
try {
|
||||
return await service.deleteDomain(domainId)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
rollbackDeployment: async (
|
||||
_: any,
|
||||
{ projectId, deploymentId }: { deploymentId: string; projectId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.rollbackDeployment(projectId, deploymentId)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
deleteDeployment: async (
|
||||
_: any,
|
||||
{ deploymentId }: { deploymentId: string }
|
||||
) => {
|
||||
try {
|
||||
return await service.deleteDeployment(deploymentId)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
addDomain: async (
|
||||
_: any,
|
||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||
) => {
|
||||
try {
|
||||
return Boolean(await service.addDomain(projectId, data))
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
updateDomain: async (
|
||||
_: any,
|
||||
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
||||
) => {
|
||||
try {
|
||||
return await service.updateDomain(domainId, data)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
authenticateGitHub: async (
|
||||
_: any,
|
||||
{ code }: { code: string },
|
||||
context: any
|
||||
) => {
|
||||
try {
|
||||
return await service.authenticateGitHub(code, context.user)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||
try {
|
||||
return service.unauthenticateGitHub(context.user, {
|
||||
gitHubToken: null
|
||||
})
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Router } from 'express'
|
||||
import { SiweMessage } from 'siwe'
|
||||
import type { Service } from '../service'
|
||||
import { authenticateUser, createUser } from '../turnkey-backend'
|
||||
|
||||
const router: Router = Router()
|
||||
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
router.get('/registration/:email', async (req, res) => {
|
||||
const service: Service = req.app.get('service')
|
||||
const user = await service.getUserByEmail(req.params.email)
|
||||
if (user) {
|
||||
return res.send({ subOrganizationId: user?.subOrgId })
|
||||
}
|
||||
|
||||
return res.sendStatus(204)
|
||||
})
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
console.log('Register', req.body)
|
||||
const { email, challenge, attestation } = req.body
|
||||
const user = await createUser(req.app.get('service'), {
|
||||
challenge,
|
||||
attestation,
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0]
|
||||
})
|
||||
req.session.address = user.id
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
router.post('/authenticate', async (req, res) => {
|
||||
console.log('Authenticate', req.body)
|
||||
const { signedWhoamiRequest } = req.body
|
||||
const user = await authenticateUser(
|
||||
req.app.get('service'),
|
||||
signedWhoamiRequest
|
||||
)
|
||||
if (user) {
|
||||
req.session.address = user.id
|
||||
res.sendStatus(200)
|
||||
} else {
|
||||
res.sendStatus(401)
|
||||
}
|
||||
})
|
||||
|
||||
//
|
||||
// SIWE Auth
|
||||
//
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature } = req.body
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature
|
||||
})
|
||||
|
||||
if (!success) {
|
||||
return res.send({ success })
|
||||
}
|
||||
const service: Service = req.app.get('service')
|
||||
const user = await service.getUserByEthAddress(data.address)
|
||||
|
||||
if (!user) {
|
||||
const newUser = await service.createUser({
|
||||
ethAddress: data.address,
|
||||
email: `${data.address}@example.com`,
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: ''
|
||||
})
|
||||
|
||||
// SIWESession from the web3modal library requires both address and chain ID
|
||||
req.session.address = newUser.id
|
||||
req.session.chainId = data.chainId
|
||||
} else {
|
||||
req.session.address = user.id
|
||||
req.session.chainId = data.chainId
|
||||
}
|
||||
|
||||
res.send({ success })
|
||||
})
|
||||
|
||||
//
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({
|
||||
address: req.session.address,
|
||||
chainId: req.session.chainId
|
||||
})
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,26 @@
|
||||
import debug from 'debug'
|
||||
import { Router } from 'express'
|
||||
|
||||
import type { Service } from '../service'
|
||||
|
||||
const log = debug('snowball:routes-github')
|
||||
const router: Router = Router()
|
||||
|
||||
/* POST GitHub webhook handler */
|
||||
// https://docs.github.com/en/webhooks/using-webhooks/handling-webhook-deliveries#javascript-example
|
||||
router.post('/webhook', async (req, res) => {
|
||||
// Server should respond with a 2XX response within 10 seconds of receiving a webhook delivery
|
||||
// If server takes longer than that to respond, then GitHub terminates the connection and considers the delivery a failure
|
||||
res.status(202).send('Accepted')
|
||||
|
||||
const service = req.app.get('service') as Service
|
||||
const githubEvent = req.headers['x-github-event']
|
||||
log(`Received GitHub webhook for event ${githubEvent}`)
|
||||
|
||||
if (githubEvent === 'push') {
|
||||
// Create deployments using push event data
|
||||
await service.handleGitHubPush(req.body)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
const router: Router = Router()
|
||||
|
||||
router.get('/version', async (_req, res) => {
|
||||
return res.send({ version: '0.0.9' })
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,337 @@
|
||||
enum Role {
|
||||
Owner
|
||||
Maintainer
|
||||
Reader
|
||||
}
|
||||
|
||||
enum Permission {
|
||||
View
|
||||
Edit
|
||||
}
|
||||
|
||||
enum Environment {
|
||||
Production
|
||||
Preview
|
||||
Development
|
||||
}
|
||||
|
||||
enum DeploymentStatus {
|
||||
Building
|
||||
Ready
|
||||
Error
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
Live
|
||||
Pending
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
name: String
|
||||
email: String!
|
||||
organizations: [Organization!]
|
||||
projects: [Project!]
|
||||
isVerified: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
gitHubToken: String
|
||||
}
|
||||
|
||||
type Organization {
|
||||
id: String!
|
||||
name: String!
|
||||
slug: String!
|
||||
projects: [Project!]
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
members: [OrganizationMember!]
|
||||
}
|
||||
|
||||
type OrganizationMember {
|
||||
id: String!
|
||||
member: User!
|
||||
role: Role!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: String!
|
||||
owner: User!
|
||||
deployments: [Deployment!]
|
||||
name: String!
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployers: [Deployer!]
|
||||
auctionId: String
|
||||
fundsReleased: Boolean
|
||||
template: String
|
||||
framework: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
webhooks: [String!]
|
||||
members: [ProjectMember!]
|
||||
environmentVariables: [EnvironmentVariable!]
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
baseDomains: [String!]
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
id: String!
|
||||
member: User!
|
||||
permissions: [Permission!]!
|
||||
isPending: Boolean!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployment {
|
||||
id: String!
|
||||
branch: String!
|
||||
commitHash: String!
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployer: Deployer
|
||||
applicationDeploymentRequestId: String
|
||||
applicationDeploymentRecordData: AppDeploymentRecordAttributes
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
createdBy: User!
|
||||
}
|
||||
|
||||
type Domain {
|
||||
id: String!
|
||||
branch: String!
|
||||
name: String!
|
||||
redirectTo: Domain
|
||||
status: DomainStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type EnvironmentVariable {
|
||||
id: String!
|
||||
environment: Environment!
|
||||
key: String!
|
||||
value: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployer {
|
||||
deployerLrn: String!
|
||||
deployerId: String!
|
||||
deployerApiUrl: String!
|
||||
minimumPayment: String
|
||||
paymentAddress: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
baseDomain: String
|
||||
}
|
||||
|
||||
type AuthResult {
|
||||
token: String!
|
||||
}
|
||||
|
||||
input AddEnvironmentVariableInput {
|
||||
environments: [Environment!]!
|
||||
key: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
input AddProjectFromTemplateInput {
|
||||
templateOwner: String!
|
||||
templateRepo: String!
|
||||
owner: String!
|
||||
name: String!
|
||||
isPrivate: Boolean!
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input AddProjectInput {
|
||||
name: String!
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
template: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
name: String
|
||||
description: String
|
||||
prodBranch: String
|
||||
organizationId: String
|
||||
webhooks: [String!]
|
||||
}
|
||||
|
||||
input AddDomainInput {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input UpdateDomainInput {
|
||||
name: String
|
||||
branch: String
|
||||
redirectToId: String
|
||||
}
|
||||
|
||||
input UpdateEnvironmentVariableInput {
|
||||
key: String
|
||||
value: String
|
||||
}
|
||||
|
||||
input AddProjectMemberInput {
|
||||
email: String!
|
||||
permissions: [Permission!]
|
||||
}
|
||||
|
||||
input UpdateProjectMemberInput {
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
input FilterDomainsInput {
|
||||
branch: String
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
type DNSRecordAttributes {
|
||||
name: String
|
||||
value: String
|
||||
request: String
|
||||
resourceType: String
|
||||
version: String
|
||||
}
|
||||
|
||||
type AppDeploymentRecordAttributes {
|
||||
application: String
|
||||
auction: String
|
||||
deployer: String
|
||||
dns: String
|
||||
meta: String
|
||||
name: String
|
||||
request: String
|
||||
type: String
|
||||
url: String
|
||||
version: String
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String
|
||||
numProviders: Int
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
projects: [Project!]
|
||||
projectsInOrganization(organizationSlug: String!): [Project!]
|
||||
project(projectId: String!): Project
|
||||
deployments(projectId: String!): [Deployment!]
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
latestDNSRecord(projectId: String!): DNSRecordAttributes
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
deployers: [Deployer]
|
||||
address: String!
|
||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||
updateProjectMember(
|
||||
projectMemberId: String!
|
||||
data: UpdateProjectMemberInput
|
||||
): Boolean!
|
||||
removeProjectMember(projectMemberId: String!): Boolean!
|
||||
addEnvironmentVariables(
|
||||
projectId: String!
|
||||
data: [AddEnvironmentVariableInput!]
|
||||
): Boolean!
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: String!
|
||||
data: UpdateEnvironmentVariableInput!
|
||||
): Boolean!
|
||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
deleteDomain(domainId: String!): Boolean!
|
||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||
deleteDeployment(deploymentId: String!): Boolean!
|
||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||
authenticateGitHub(code: String!): AuthResult!
|
||||
unauthenticateGitHub: Boolean!
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createServer } from 'node:http'
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError
|
||||
} from 'apollo-server-core'
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import cors from 'cors'
|
||||
import debug from 'debug'
|
||||
import express from 'express'
|
||||
import session from 'express-session'
|
||||
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema'
|
||||
import type { TypeSource } from '@graphql-tools/utils'
|
||||
|
||||
import type { ServerConfig } from './config'
|
||||
import { DEFAULT_GQL_PATH } from './constants'
|
||||
import authRouter from './routes/auth'
|
||||
import githubRouter from './routes/github'
|
||||
import stagingRouter from './routes/staging'
|
||||
import type { Service } from './service'
|
||||
|
||||
const log = debug('snowball:server')
|
||||
|
||||
// Set cookie expiration to 1 month in milliseconds
|
||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
address: string
|
||||
chainId: number
|
||||
}
|
||||
}
|
||||
|
||||
export const createAndStartServer = async (
|
||||
serverConfig: ServerConfig,
|
||||
typeDefs: TypeSource,
|
||||
resolvers: any,
|
||||
service: Service
|
||||
): Promise<ApolloServer> => {
|
||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig
|
||||
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session
|
||||
|
||||
const app = express()
|
||||
|
||||
// Create HTTP server
|
||||
const httpServer = createServer(app)
|
||||
|
||||
// Create the schema
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers
|
||||
})
|
||||
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
csrfPrevention: true,
|
||||
context: async ({ req }) => {
|
||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||
|
||||
const { address } = req.session
|
||||
|
||||
if (!address) {
|
||||
throw new AuthenticationError('Unauthorized: No active session')
|
||||
}
|
||||
|
||||
const user = await service.getUser(address)
|
||||
return { user }
|
||||
},
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||
ApolloServerPluginLandingPageLocalDefault({ embed: true })
|
||||
]
|
||||
})
|
||||
|
||||
await server.start()
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: appOriginUrl,
|
||||
credentials: true
|
||||
})
|
||||
)
|
||||
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: secret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
domain: domain || undefined,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
|
||||
}
|
||||
}
|
||||
|
||||
if (trustProxy) {
|
||||
// trust first proxy
|
||||
app.set('trust proxy', 1)
|
||||
}
|
||||
|
||||
app.use(session(sessionOptions))
|
||||
|
||||
server.applyMiddleware({
|
||||
app: app as any,
|
||||
path: gqlPath,
|
||||
cors: {
|
||||
origin: [appOriginUrl],
|
||||
credentials: true
|
||||
}
|
||||
})
|
||||
|
||||
app.use(express.json())
|
||||
|
||||
app.set('service', service)
|
||||
app.use('/auth', authRouter)
|
||||
app.use('/api/github', githubRouter)
|
||||
app.use('/staging', stagingRouter)
|
||||
|
||||
app.use((err: any, _req: any, res: any, _next: any) => {
|
||||
console.error(err)
|
||||
res.status(500).json({ error: err.message })
|
||||
})
|
||||
|
||||
httpServer.listen(port, host, () => {
|
||||
log(`Server is listening on ${host}:${port}${server.graphqlPath}`)
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
import { Turnkey, type TurnkeyApiTypes } from '@turnkey/sdk-server'
|
||||
|
||||
// Default path for the first Ethereum address in a new HD wallet.
|
||||
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
// - Purpose is a constant set to 44' following the BIP43 recommendation.
|
||||
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
// - Account, Change, and Address Index are set to 0
|
||||
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server'
|
||||
import type { Service } from './service'
|
||||
import { getConfig } from './utils'
|
||||
|
||||
type TAttestation = TurnkeyApiTypes['v1Attestation']
|
||||
|
||||
type CreateUserParams = {
|
||||
userName: string
|
||||
userEmail: string
|
||||
challenge: string
|
||||
attestation: TAttestation
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
service: Service,
|
||||
{ userName, userEmail, challenge, attestation }: CreateUserParams
|
||||
) {
|
||||
try {
|
||||
if (await service.getUserByEmail(userEmail)) {
|
||||
throw new Error(`User already exists: ${userEmail}`)
|
||||
}
|
||||
|
||||
const config = await getConfig()
|
||||
const turnkey = new Turnkey(config.turnkey)
|
||||
|
||||
const apiClient = turnkey.api()
|
||||
|
||||
const walletName = 'Default ETH Wallet'
|
||||
|
||||
const createSubOrgResponse = await apiClient.createSubOrganization({
|
||||
subOrganizationName: `Default SubOrg for ${userEmail}`,
|
||||
rootQuorumThreshold: 1,
|
||||
rootUsers: [
|
||||
{
|
||||
userName,
|
||||
userEmail,
|
||||
apiKeys: [],
|
||||
authenticators: [
|
||||
{
|
||||
authenticatorName: 'Passkey',
|
||||
challenge,
|
||||
attestation
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
wallet: {
|
||||
walletName: walletName,
|
||||
accounts: DEFAULT_ETHEREUM_ACCOUNTS
|
||||
}
|
||||
})
|
||||
|
||||
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId)
|
||||
const wallet = refineNonNull(createSubOrgResponse.wallet)
|
||||
|
||||
const result = {
|
||||
id: wallet.walletId,
|
||||
address: wallet.addresses[0],
|
||||
subOrgId: subOrgId
|
||||
}
|
||||
console.log('Turnkey success', result)
|
||||
|
||||
const user = await service.createUser({
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
subOrgId,
|
||||
ethAddress: wallet.addresses[0],
|
||||
turnkeyWalletId: wallet.walletId
|
||||
})
|
||||
console.log('New user', user)
|
||||
|
||||
return user
|
||||
} catch (e) {
|
||||
console.error('Failed to create user:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
service: Service,
|
||||
signedWhoamiRequest: {
|
||||
url: string
|
||||
body: any
|
||||
stamp: {
|
||||
stampHeaderName: string
|
||||
stampHeaderValue: string
|
||||
}
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const tkRes = await fetch(signedWhoamiRequest.url, {
|
||||
method: 'POST',
|
||||
body: signedWhoamiRequest.body,
|
||||
headers: {
|
||||
[signedWhoamiRequest.stamp.stampHeaderName]:
|
||||
signedWhoamiRequest.stamp.stampHeaderValue
|
||||
}
|
||||
})
|
||||
console.log('AUTH RESULT', tkRes.status)
|
||||
if (tkRes.status !== 200) {
|
||||
console.log(await tkRes.text())
|
||||
return null
|
||||
}
|
||||
const orgId = (await tkRes.json()).organizationId
|
||||
const user = await service.getUserBySubOrgId(orgId)
|
||||
return user
|
||||
} catch (e) {
|
||||
console.error('Failed to authenticate:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function refineNonNull<T>(
|
||||
input: T | null | undefined,
|
||||
errorMessage?: string
|
||||
): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`)
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
export interface PackageJSON {
|
||||
name: string
|
||||
version: string
|
||||
author?: string
|
||||
description?: string
|
||||
homepage?: string
|
||||
license?: string
|
||||
repository?: string
|
||||
}
|
||||
|
||||
export interface GitRepositoryDetails {
|
||||
id: number
|
||||
name: string
|
||||
full_name: string
|
||||
visibility?: string
|
||||
updated_at?: string | null
|
||||
default_branch?: string
|
||||
}
|
||||
|
||||
export interface GitPushEventPayload {
|
||||
repository: GitRepositoryDetails
|
||||
ref: string
|
||||
head_commit: {
|
||||
id: string
|
||||
message: string
|
||||
}
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string
|
||||
auction: string
|
||||
deployer: string
|
||||
dns: string
|
||||
meta: string
|
||||
name: string
|
||||
request: string
|
||||
type: string
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface DNSRecordAttributes {
|
||||
name: string
|
||||
value: string
|
||||
request: string
|
||||
resourceType: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface RegistryDNSRecordAttributes {
|
||||
name: string
|
||||
value: string
|
||||
request: string
|
||||
resource_type: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecordAttributes {
|
||||
deployment: string
|
||||
request: string
|
||||
type: 'ApplicationDeploymentRemovalRecord'
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface RegistryRecord {
|
||||
id: string
|
||||
names: string[] | null
|
||||
owners: string[]
|
||||
bondId: string
|
||||
createTime: string
|
||||
expiryTime: string
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRecordAttributes
|
||||
}
|
||||
|
||||
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||
attributes: AppDeploymentRemovalRecordAttributes
|
||||
}
|
||||
|
||||
export interface DNSRecord extends RegistryRecord {
|
||||
attributes: RegistryDNSRecordAttributes
|
||||
}
|
||||
|
||||
export interface AddProjectFromTemplateInput {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
owner: string
|
||||
name: string
|
||||
isPrivate: boolean
|
||||
paymentAddress: string
|
||||
txHash: string
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string
|
||||
numProviders: number
|
||||
}
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
environments: string[]
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface DeployerRecord {
|
||||
id: string
|
||||
names: string[]
|
||||
owners: string[]
|
||||
bondId: string
|
||||
createTime: string
|
||||
expiryTime: string
|
||||
attributes: {
|
||||
apiUrl: string
|
||||
minimumPayment: string | null
|
||||
name: string
|
||||
paymentAddress: string
|
||||
publicKey: string
|
||||
type: string
|
||||
version: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import assert from 'node:assert'
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import fs from 'fs-extra'
|
||||
import type { Octokit } from 'octokit'
|
||||
import toml from 'toml'
|
||||
import type {
|
||||
DataSource,
|
||||
DeepPartial,
|
||||
EntityTarget,
|
||||
ObjectLiteral
|
||||
} from 'typeorm'
|
||||
|
||||
import type { Config } from './config'
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants'
|
||||
import type { PackageJSON } from './types'
|
||||
|
||||
const log = debug('snowball:utils')
|
||||
|
||||
export async function getConfig() {
|
||||
// TODO: get config path using cli
|
||||
return await _getConfig<Config>(DEFAULT_CONFIG_FILE_PATH)
|
||||
}
|
||||
|
||||
const _getConfig = async <ConfigType>(
|
||||
configFile: string
|
||||
): Promise<ConfigType> => {
|
||||
const configFilePath = path.resolve(configFile)
|
||||
const fileExists = await fs.pathExists(configFilePath)
|
||||
if (!fileExists) {
|
||||
throw new Error(`Config file not found: ${configFilePath}`)
|
||||
}
|
||||
|
||||
const config = toml.parse(await fs.readFile(configFilePath, 'utf8'))
|
||||
log('config', JSON.stringify(config, null, 2))
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export const checkFileExists = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.F_OK)
|
||||
return true
|
||||
} catch (err) {
|
||||
log(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const getEntities = async (filePath: string): Promise<any> => {
|
||||
const entitiesData = await fs.readFile(filePath, 'utf-8')
|
||||
const entities = JSON.parse(entitiesData)
|
||||
return entities
|
||||
}
|
||||
|
||||
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
dataSource: DataSource,
|
||||
entities: any,
|
||||
relations?: any | undefined
|
||||
): Promise<Entity[]> => {
|
||||
const entityRepository = dataSource.getRepository(entityType)
|
||||
|
||||
const savedEntity: Entity[] = []
|
||||
|
||||
for (const entityData of entities) {
|
||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>)
|
||||
|
||||
if (relations) {
|
||||
for (const field in relations) {
|
||||
const valueIndex = `${field}Index`
|
||||
|
||||
entity = {
|
||||
...entity,
|
||||
[field]: relations[field][entityData[valueIndex]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const dbEntity = await entityRepository.save(entity)
|
||||
savedEntity.push(dbEntity)
|
||||
}
|
||||
|
||||
return savedEntity
|
||||
}
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
export const getRepoDetails = async (
|
||||
octokit: Octokit,
|
||||
repository: string,
|
||||
commitHash: string | undefined
|
||||
): Promise<{
|
||||
repo: string
|
||||
packageJSON: PackageJSON
|
||||
repoUrl: string
|
||||
}> => {
|
||||
const [owner, repo] = repository.split('/')
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: commitHash
|
||||
})
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found')
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file')
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content))
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json")
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
})
|
||||
).data.html_url
|
||||
|
||||
return {
|
||||
repo,
|
||||
packageJSON,
|
||||
repoUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
||||
export const registryTransactionWithRetry = async (
|
||||
txMethod: () => Promise<any>
|
||||
): Promise<any> => {
|
||||
try {
|
||||
return await txMethod()
|
||||
} catch (error: any) {
|
||||
if (!error.message.includes('account sequence mismatch')) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.error(
|
||||
'Transaction failed due to account sequence mismatch. Retrying...'
|
||||
)
|
||||
|
||||
try {
|
||||
return await txMethod()
|
||||
} catch (retryError: any) {
|
||||
throw new Error(
|
||||
`Transaction failed again after retry: ${retryError.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as fs from 'node:fs/promises'
|
||||
import debug from 'debug'
|
||||
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:delete-database')
|
||||
|
||||
const deleteFile = async (filePath: string) => {
|
||||
await fs.unlink(filePath)
|
||||
log(`File ${filePath} has been deleted.`)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig()
|
||||
|
||||
deleteFile(config.database.dbPath)
|
||||
}
|
||||
|
||||
main().catch((err) => log(err))
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 0,
|
||||
"createdByIndex": 0,
|
||||
"id": "ffhae3zq",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-ffhae3zq.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 1,
|
||||
"createdByIndex": 0,
|
||||
"id": "vehagei8",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-vehagei8.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": 2,
|
||||
"createdByIndex": 0,
|
||||
"id": "qmgekyte",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-qmgekyte.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"domainIndex": null,
|
||||
"createdByIndex": 0,
|
||||
"id": "f8wsyim6",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "prod",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-f8wsyim6.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 3,
|
||||
"createdByIndex": 1,
|
||||
"id": "eO8cckxk",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-eO8cckxk.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 4,
|
||||
"createdByIndex": 1,
|
||||
"id": "yaq0t5yw",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-yaq0t5yw.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"domainIndex": 5,
|
||||
"createdByIndex": 1,
|
||||
"id": "hwwr6sbx",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "testProject-2-hwwr6sbx.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 9,
|
||||
"createdByIndex": 2,
|
||||
"id": "ndxje48a",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "main",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-ndxje48a.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 7,
|
||||
"createdByIndex": 2,
|
||||
"id": "gtgpgvei",
|
||||
"status": "Ready",
|
||||
"environment": "Preview",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-gtgpgvei.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"domainIndex": 8,
|
||||
"createdByIndex": 2,
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Development",
|
||||
"isCurrent": false,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-b4bpthjr.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"domainIndex": 6,
|
||||
"createdByIndex": 2,
|
||||
"id": "b4bpthjr",
|
||||
"status": "Ready",
|
||||
"environment": "Production",
|
||||
"isCurrent": true,
|
||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||
"applicationRecordData": {},
|
||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||
"applicationDeploymentRequestData": {},
|
||||
"branch": "test",
|
||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||
"commitMessage": "subscription added",
|
||||
"url": "iglootools-b4bpthjr.snowball.xyz"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Production"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "ABC",
|
||||
"value": "ABC",
|
||||
"environment": "Preview"
|
||||
},
|
||||
{
|
||||
"projectIndex": 4,
|
||||
"key": "XYZ",
|
||||
"value": "abc3",
|
||||
"environment": "Preview"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||
"name": "Deploy Tools",
|
||||
"slug": "deploy-tools"
|
||||
}
|
||||
]
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "example.snowballtools.xyz",
|
||||
"status": "Live",
|
||||
"branch": "main"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "example.org",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 3,
|
||||
"name": "iglootools-2.com",
|
||||
"status": "Pending",
|
||||
"branch": "test"
|
||||
}
|
||||
]
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 0,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 1,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 2,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 0,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 3,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 1,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View"],
|
||||
"isPending": false
|
||||
},
|
||||
{
|
||||
"memberIndex": 2,
|
||||
"projectIndex": 4,
|
||||
"permissions": ["View", "Edit"],
|
||||
"isPending": false
|
||||
}
|
||||
]
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
[
|
||||
{
|
||||
"ownerIndex": 0,
|
||||
"organizationIndex": 0,
|
||||
"name": "testProject",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test",
|
||||
"template": "webapp",
|
||||
"framework": "test",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "testProject.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 1,
|
||||
"organizationIndex": 0,
|
||||
"name": "testProject-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-2",
|
||||
"template": "webapp",
|
||||
"framework": "test-2",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "testProject-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 2,
|
||||
"organizationIndex": 0,
|
||||
"name": "iglootools",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-3",
|
||||
"template": "webapp",
|
||||
"framework": "test-3",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "iglootools.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 1,
|
||||
"organizationIndex": 0,
|
||||
"name": "iglootools-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-4",
|
||||
"template": "webapp",
|
||||
"framework": "test-4",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "iglootools-2.snowball.xyz"
|
||||
},
|
||||
{
|
||||
"ownerIndex": 0,
|
||||
"organizationIndex": 1,
|
||||
"name": "snowball-2",
|
||||
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||
"prodBranch": "main",
|
||||
"description": "test-5",
|
||||
"template": "webapp",
|
||||
"framework": "test-5",
|
||||
"webhooks": [],
|
||||
"icon": "",
|
||||
"subDomain": "snowball-2.snowball.xyz"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"projectIndex": 0,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 1,
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 1,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 3,
|
||||
"branch": "test"
|
||||
},
|
||||
{
|
||||
"projectIndex": 2,
|
||||
"name": "www.example.org",
|
||||
"status": "Pending",
|
||||
"redirectToIndex": 5,
|
||||
"branch": "test"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 0,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Maintainer",
|
||||
"memberIndex": 1,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 2,
|
||||
"organizationIndex": 0
|
||||
},
|
||||
{
|
||||
"role": "Owner",
|
||||
"memberIndex": 0,
|
||||
"organizationIndex": 1
|
||||
}
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
[
|
||||
{
|
||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||
"email": "snowball@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||
"email": "alice@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
||||
},
|
||||
{
|
||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
||||
"email": "bob@snowballtools.xyz",
|
||||
"isVerified": true,
|
||||
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,176 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Deployment } from '../src/entity/Deployment'
|
||||
import { Domain } from '../src/entity/Domain'
|
||||
import { EnvironmentVariable } from '../src/entity/EnvironmentVariable'
|
||||
import { Organization } from '../src/entity/Organization'
|
||||
import { Project } from '../src/entity/Project'
|
||||
import { ProjectMember } from '../src/entity/ProjectMember'
|
||||
import { User } from '../src/entity/User'
|
||||
import { UserOrganization } from '../src/entity/UserOrganization'
|
||||
import {
|
||||
checkFileExists,
|
||||
getConfig,
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils'
|
||||
|
||||
const log = debug('snowball:initialize-database')
|
||||
|
||||
const USER_DATA_PATH = './fixtures/users.json'
|
||||
const PROJECT_DATA_PATH = './fixtures/projects.json'
|
||||
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json'
|
||||
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json'
|
||||
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json'
|
||||
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json'
|
||||
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'
|
||||
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'
|
||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'
|
||||
|
||||
const generateTestData = async (dataSource: DataSource) => {
|
||||
const userEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_DATA_PATH)
|
||||
)
|
||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities)
|
||||
|
||||
const orgEntities = await getEntities(
|
||||
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities)
|
||||
|
||||
const projectRelations = {
|
||||
owner: savedUsers,
|
||||
organization: savedOrgs
|
||||
}
|
||||
|
||||
const projectEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||
)
|
||||
const savedProjects = await loadAndSaveData(
|
||||
Project,
|
||||
dataSource,
|
||||
projectEntities,
|
||||
projectRelations
|
||||
)
|
||||
|
||||
const domainRepository = dataSource.getRepository(Domain)
|
||||
|
||||
const domainPrimaryRelations = {
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const primaryDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||
)
|
||||
const savedPrimaryDomains = await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
primaryDomainsEntities,
|
||||
domainPrimaryRelations
|
||||
)
|
||||
|
||||
const domainRedirectedRelations = {
|
||||
project: savedProjects,
|
||||
redirectTo: savedPrimaryDomains
|
||||
}
|
||||
|
||||
const redirectDomainsEntities = await getEntities(
|
||||
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
Domain,
|
||||
dataSource,
|
||||
redirectDomainsEntities,
|
||||
domainRedirectedRelations
|
||||
)
|
||||
|
||||
const savedDomains = await domainRepository.find()
|
||||
|
||||
const userOrganizationRelations = {
|
||||
member: savedUsers,
|
||||
organization: savedOrgs
|
||||
}
|
||||
|
||||
const userOrganizationsEntities = await getEntities(
|
||||
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
UserOrganization,
|
||||
dataSource,
|
||||
userOrganizationsEntities,
|
||||
userOrganizationRelations
|
||||
)
|
||||
|
||||
const projectMemberRelations = {
|
||||
member: savedUsers,
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const projectMembersEntities = await getEntities(
|
||||
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
ProjectMember,
|
||||
dataSource,
|
||||
projectMembersEntities,
|
||||
projectMemberRelations
|
||||
)
|
||||
|
||||
const deploymentRelations = {
|
||||
project: savedProjects,
|
||||
domain: savedDomains,
|
||||
createdBy: savedUsers
|
||||
}
|
||||
|
||||
const deploymentsEntities = await getEntities(
|
||||
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
Deployment,
|
||||
dataSource,
|
||||
deploymentsEntities,
|
||||
deploymentRelations
|
||||
)
|
||||
|
||||
const environmentVariableRelations = {
|
||||
project: savedProjects
|
||||
}
|
||||
|
||||
const environmentVariablesEntities = await getEntities(
|
||||
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||
)
|
||||
await loadAndSaveData(
|
||||
EnvironmentVariable,
|
||||
dataSource,
|
||||
environmentVariablesEntities,
|
||||
environmentVariableRelations
|
||||
)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig()
|
||||
const isDbPresent = await checkFileExists(config.database.dbPath)
|
||||
|
||||
if (!isDbPresent) {
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: config.database.dbPath,
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
await generateTestData(dataSource)
|
||||
log('Data loaded successfully')
|
||||
} else {
|
||||
log('WARNING: Database already exists')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import debug from 'debug'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:initialize-registry')
|
||||
|
||||
const DENOM = 'alnt'
|
||||
const BOND_AMOUNT = '1000000000'
|
||||
|
||||
async function main() {
|
||||
const { registryConfig } = await getConfig()
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority]
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey)
|
||||
log('bondId:', bondId)
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee)
|
||||
log('Reserved authority name:', name)
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
log(`Bond ${bondId} set for authority ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Environment
|
||||
} from '../src/entity/Deployment'
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:publish-deploy-records')
|
||||
|
||||
async function main() {
|
||||
const { registryConfig, database } = await getConfig()
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: database.dbPath,
|
||||
synchronize: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
const deploymentRepository = dataSource.getRepository(Deployment)
|
||||
const deployments = await deploymentRepository.find({
|
||||
relations: {
|
||||
project: true
|
||||
},
|
||||
where: {
|
||||
status: DeploymentStatus.Building
|
||||
}
|
||||
})
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`
|
||||
|
||||
const applicationDeploymentRecord = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
version: '0.0.1',
|
||||
name: deployment.applicationRecordData.name,
|
||||
application: deployment.applicationRecordId,
|
||||
|
||||
// TODO: Create DNS record
|
||||
dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm',
|
||||
|
||||
// Using dummy values
|
||||
meta: JSON.stringify({
|
||||
config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
|
||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||
}),
|
||||
|
||||
request: deployment.applicationDeploymentRequestId,
|
||||
url
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
}
|
||||
|
||||
log('Application deployment record data:', applicationDeploymentRecord)
|
||||
log(`Application deployment record published: ${result.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import path from 'node:path'
|
||||
import debug from 'debug'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { Registry, parseGasAndFees } from '@cerc-io/registry-sdk'
|
||||
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment'
|
||||
import { getConfig } from '../src/utils'
|
||||
|
||||
const log = debug('snowball:publish-deployment-removal-records')
|
||||
|
||||
async function main() {
|
||||
const { registryConfig, database } = await getConfig()
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
)
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: database.dbPath,
|
||||
synchronize: true,
|
||||
entities: [path.join(__dirname, '../src/entity/*')]
|
||||
})
|
||||
|
||||
await dataSource.initialize()
|
||||
|
||||
const deploymentRepository = dataSource.getRepository(Deployment)
|
||||
const deployments = await deploymentRepository.find({
|
||||
relations: {
|
||||
project: true
|
||||
},
|
||||
where: {
|
||||
status: DeploymentStatus.Deleting
|
||||
}
|
||||
})
|
||||
|
||||
for await (const deployment of deployments) {
|
||||
const applicationDeploymentRemovalRecord = {
|
||||
type: 'ApplicationDeploymentRemovalRecord',
|
||||
version: '1.0.0',
|
||||
deployment: deployment.applicationDeploymentRecordId,
|
||||
request: deployment.applicationDeploymentRemovalRequestId
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees)
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRecord,
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
)
|
||||
|
||||
log(
|
||||
'Application deployment removal record data:',
|
||||
applicationDeploymentRemovalRecord
|
||||
)
|
||||
log(`Application deployment removal record published: ${result.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err)
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"baseUrl": ".",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["dist", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# deployer
|
||||
|
||||
- Install dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
```bash
|
||||
brew install jq # if you do not have jq installed already
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
|
||||
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
|
||||
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
||||
|
||||
## Notes
|
||||
|
||||
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||
|
||||
```yml
|
||||
record:
|
||||
...
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
...
|
||||
```
|
||||
|
||||
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
- Check deployment status in [web-app deployer](https://console.laconic.com/deployer).
|
||||
- Check records in [registry console app](https://console.laconic.com/#/registry).
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Check balances
|
||||
|
||||
```bash
|
||||
# Account balance
|
||||
pnpm laconic registry account get
|
||||
|
||||
# Bond balance
|
||||
pnpm laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
pnpm laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["../../biome.json"],
|
||||
"files": {
|
||||
"ignore": ["dist/**/*", "node_modules/**/*", ".turbo/**/*"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
registry:
|
||||
restEndpoint: 'http://console.laconic.com:1317'
|
||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||
chainId: laconic_9000-1
|
||||
gas:
|
||||
fees:
|
||||
gasPrice: 1
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey:
|
||||
bondId:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash for a branch
|
||||
BRANCH_NAME="main"
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||
|
||||
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
|
||||
|
||||
# Extract version from frontend package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../../apps/deploy-fe/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(pnpm --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(pnpm --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(pnpm --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
|
||||
echo "Paying address: $paymentAddress with amount $paymentAmount..."
|
||||
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(pnpm --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy.laconic.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.laconic.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23li4NtYybQlF6u5Dk
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
exit $rc
|
||||
fi
|
||||
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
exit $rc
|
||||
fi
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(pnpm --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
exit $rc
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get payment address for deployer
|
||||
paymentAddress=$(pnpm --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||
paymentAmount=$(pnpm --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||
# Pay deployer if paymentAmount is not null
|
||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||
payment=$(pnpm --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||
|
||||
# Extract the transaction hash
|
||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||
echo "Paid deployer with txHash as $txHash"
|
||||
|
||||
else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
|
||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
payment: $txHash
|
||||
EOF
|
||||
|
||||
RECORD_FILE=records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
fi
|
||||
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@qwrk/deployer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test:deploy": "./test/test-webapp-deployment-undeployment.sh",
|
||||
"deploy:frontend": "./deploy-frontend.sh",
|
||||
"deploy:frontend:staging": "./deploy-frontend.staging.sh",
|
||||
"deploy:frontend:vaasl": "./deploy-frontend.vaasl.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||
}
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=records/application-deployment-removal-request.yml
|
||||
# TODO: Pass deployment record ID as arg
|
||||
DEPLOYMENT_RECORD_ID=bafyreidjho77xeczaqpyawhc4wbpm5it5atibtuxk6ost6vnpu2svlp3ka
|
||||
|
||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployer: $DEPLOYER_LRN
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deployment removal successful"
|
||||
@@ -0,0 +1,23 @@
|
||||
# deployer test
|
||||
|
||||
Check if the live web app deployer is in a working state
|
||||
|
||||
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
|
||||
- Config used: [../config.yml](../config.yml)
|
||||
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
||||
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
||||
- Fetch the latest version of `deployment-test-app` from registry and increment `ApplicationRecord` version
|
||||
- Publish the resulting `ApplicationRecord` record
|
||||
- Set names to the record and check name resolution
|
||||
- Publish the `ApplicationDeploymentRequest` record
|
||||
- Check that the deployment occurs
|
||||
- Check that a `ApplicationDeploymentRecord` is created
|
||||
- Check that the deployment record has correct `ApplicationRecord` id
|
||||
- Check that the URL present in deployment record is active
|
||||
- Create and publish a [`ApplicationDeploymentRemovalRequest`](./records/application-deployment-removal-request.yml) record
|
||||
- Check that the deployment is removed
|
||||
- Check that a `ApplicationDeploymentRemovalRecord` is created
|
||||
- Check that the deployment URL goes down
|
||||
- The test script is run in a GitHub CI [workflow](../../../.github/workflows/test-app-deployment.yaml) that:
|
||||
- Is scheduled to run everyday on the default (`main`) branch or can be triggered manually
|
||||
- Sends Slack alerts to configured channels on failure
|
||||
@@ -0,0 +1,4 @@
|
||||
record:
|
||||
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
@@ -0,0 +1,15 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: "1.0.0"
|
||||
name: deployment-test-app@0.1.24
|
||||
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
||||
repository: "https://github.com/snowball-tools/test-progressive-web-app"
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||
@@ -0,0 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.1
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
|
||||
app_type: webapp
|
||||
name: deployment-test-app
|
||||
app_version: 0.1.24
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Fetch the package.json file content
|
||||
# Extract version from package.json content
|
||||
package_json=$(wget -qO- "$REPO_URL/raw/$LATEST_HASH/package.json")
|
||||
PACKAGE_VERSION=$(echo "$package_json" | jq -r '.version')
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=apps/deployer/config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
APP_NAME=deployment-test-app
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
RECORD_FILE=apps/deployer/test/records/application-record.yml
|
||||
|
||||
cat >$RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: $APP_NAME
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
REQUEST_RECORD_FILE=apps/deployer/test/records/application-deployment-request.yml
|
||||
|
||||
cat >$REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: $APP_NAME@$PACKAGE_VERSION
|
||||
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
echo "Record files generated successfully."
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
pnpm --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
|
||||
# Check if record exists for REGISTRY_APP_LRN
|
||||
APP_RECORD=$(pnpm --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that a ApplicationDeploymentRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
deployment_records_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_deployment_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRecord for deployment request $DEPLOYMENT_REQUEST_ID not found, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRecord found"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
DEPLOYMENT_RECORD_ID=$(echo $deployment_records_response | jq -r '.[0].id')
|
||||
echo $DEPLOYMENT_RECORD_ID
|
||||
|
||||
# Check if ApplicationDeploymentRecord has the correct record id
|
||||
fetched_application_record_id=$(echo $deployment_records_response | jq -r '.[0].attributes.application')
|
||||
if [ "$fetched_application_record_id" = "$RECORD_ID" ]; then
|
||||
echo "ApplicationRecord id matched"
|
||||
else
|
||||
echo "ApplicationRecord id does not match, expected: $RECORD_ID, received: $fetched_application_record_id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the url present in ApplicationDeploymentRecord is active
|
||||
fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
||||
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=10
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "200" ]; then
|
||||
echo "Deployment URL $fetched_url is active"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is not active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=apps/deployer/test/records/application-deployment-removal-request.yml
|
||||
|
||||
cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(pnpm --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(pnpm --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the application url is down after deployment removal
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=5
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "404" ]; then
|
||||
echo "Deployment URL $fetched_url is down"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is still active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Test successful"
|
||||
+21
-3
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": ["node_modules", "dist", "build", ".next"]
|
||||
"ignore": ["node_modules", "dist", "build", ".next", ".turbo"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
@@ -21,7 +21,10 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
"recommended": true,
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
@@ -37,5 +40,20 @@
|
||||
"enabled": true,
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["apps/backend/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@workspace/typescript-config": "workspace:*",
|
||||
"chalk": "5",
|
||||
"lefthook": "^1.11.2",
|
||||
|
||||
Generated
+5300
-123
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,25 @@ packages:
|
||||
- apps/*
|
||||
- services/*
|
||||
onlyBuiltDependencies:
|
||||
- '@apollo/protobufjs'
|
||||
- '@biomejs/biome'
|
||||
- '@biomejs/cli-darwin-arm64'
|
||||
- '@tharsis/address-converter'
|
||||
- '@tharsis/eip712'
|
||||
- '@tharsis/proto'
|
||||
- '@tharsis/provider'
|
||||
- '@tharsis/transactions'
|
||||
- better-sqlite3
|
||||
- chalk
|
||||
- core-js-pure
|
||||
- esbuild
|
||||
- keccak
|
||||
- lefthook
|
||||
- lefthook-darwin-arm64
|
||||
- protobufjs
|
||||
- secp256k1
|
||||
- sharp
|
||||
- tiny-secp256k1
|
||||
- tree-sitter
|
||||
- tree-sitter-javascript
|
||||
- tree-sitter-typescript
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||
"extends": ["../../biome.json"],
|
||||
"files": {
|
||||
"ignore": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@workspace/gql-client",
|
||||
"license": "UNLICENSED",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"lint": "biome check .",
|
||||
"format": "biome format . --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.4",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
type DefaultOptions,
|
||||
InMemoryCache,
|
||||
type NormalizedCacheObject
|
||||
} from '@apollo/client'
|
||||
|
||||
import * as mutations from './mutations'
|
||||
import * as queries from './queries'
|
||||
import type * as types from './types'
|
||||
|
||||
export interface GraphQLConfig {
|
||||
gqlEndpoint: string
|
||||
}
|
||||
|
||||
// TODO: check options
|
||||
const defaultOptions: DefaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'ignore'
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all'
|
||||
}
|
||||
}
|
||||
|
||||
export class GQLClient {
|
||||
private client: ApolloClient<NormalizedCacheObject>
|
||||
|
||||
constructor(config: GraphQLConfig) {
|
||||
this.client = new ApolloClient({
|
||||
uri: config.gqlEndpoint,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions,
|
||||
credentials: 'include'
|
||||
})
|
||||
}
|
||||
|
||||
async getUser(): Promise<types.GetUserResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getUser
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getProject(projectId: string): Promise<types.GetProjectResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getProjectsInOrganization(
|
||||
organizationSlug: string
|
||||
): Promise<types.GetProjectsInOrganizationResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getProjectsInOrganization,
|
||||
variables: {
|
||||
organizationSlug
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getOrganizations(): Promise<types.GetOrganizationsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getOrganizations
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getDeployments(
|
||||
projectId: string
|
||||
): Promise<types.GetDeploymentsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getDeployments,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getEnvironmentVariables(
|
||||
projectId: string
|
||||
): Promise<types.GetEnvironmentVariablesResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getEnvironmentVariables,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getProjectMembers(
|
||||
projectId: string
|
||||
): Promise<types.GetProjectMembersResponse> {
|
||||
const result = await this.client.query({
|
||||
query: queries.getProjectMembers,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async addProjectMember(
|
||||
projectId: string,
|
||||
data: types.AddProjectMemberInput
|
||||
): Promise<types.AddProjectMemberResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addProjectMember,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async updateProjectMember(
|
||||
projectMemberId: string,
|
||||
data: types.UpdateProjectMemberInput
|
||||
): Promise<types.UpdateProjectMemberResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.updateProjectMember,
|
||||
variables: {
|
||||
projectMemberId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async removeProjectMember(
|
||||
projectMemberId: string
|
||||
): Promise<types.RemoveProjectMemberResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.removeProjectMember,
|
||||
variables: {
|
||||
projectMemberId
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async searchProjects(
|
||||
searchText: string
|
||||
): Promise<types.SearchProjectsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.searchProjects,
|
||||
variables: {
|
||||
searchText
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async addEnvironmentVariables(
|
||||
projectId: string,
|
||||
data: types.AddEnvironmentVariableInput[]
|
||||
): Promise<types.AddEnvironmentVariablesResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addEnvironmentVariables,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable(
|
||||
environmentVariableId: string,
|
||||
data: types.UpdateEnvironmentVariableInput
|
||||
): Promise<types.UpdateEnvironmentVariableResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.updateEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async removeEnvironmentVariable(
|
||||
environmentVariableId: string
|
||||
): Promise<types.RemoveEnvironmentVariableResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.removeEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async updateDeploymentToProd(
|
||||
deploymentId: string
|
||||
): Promise<types.UpdateDeploymentToProdResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.updateDeploymentToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async addProjectFromTemplate(
|
||||
organizationSlug: string,
|
||||
data: types.AddProjectFromTemplateInput,
|
||||
lrn?: string,
|
||||
auctionParams?: types.AuctionParams,
|
||||
environmentVariables?: types.AddEnvironmentVariableInput[]
|
||||
): Promise<types.AddProjectFromTemplateResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addProjectFromTemplate,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async addProject(
|
||||
organizationSlug: string,
|
||||
data: types.AddProjectInput,
|
||||
lrn?: string,
|
||||
auctionParams?: types.AuctionParams,
|
||||
environmentVariables?: types.AddEnvironmentVariableInput[]
|
||||
): Promise<types.AddProjectResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addProject,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async updateProject(
|
||||
projectId: string,
|
||||
data: types.UpdateProjectInput
|
||||
): Promise<types.UpdateProjectResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.updateProjectMutation,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async updateDomain(
|
||||
domainId: string,
|
||||
data: types.UpdateDomainInput
|
||||
): Promise<types.UpdateDomainResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.updateDomainMutation,
|
||||
variables: {
|
||||
domainId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async redeployToProd(
|
||||
deploymentId: string
|
||||
): Promise<types.RedeployToProdResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.redeployToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async deleteProject(projectId: string): Promise<types.DeleteProjectResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.deleteProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async deleteDomain(domainId: string): Promise<types.DeleteDomainResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.deleteDomain,
|
||||
variables: {
|
||||
domainId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async rollbackDeployment(
|
||||
projectId: string,
|
||||
deploymentId: string
|
||||
): Promise<types.RollbackDeploymentResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.rollbackDeployment,
|
||||
variables: {
|
||||
projectId,
|
||||
deploymentId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async deleteDeployment(
|
||||
deploymentId: string
|
||||
): Promise<types.DeleteDeploymentResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.deleteDeployment,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async addDomain(
|
||||
projectId: string,
|
||||
data: types.AddDomainInput
|
||||
): Promise<types.AddDomainResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addDomain,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
async getDomains(
|
||||
projectId: string,
|
||||
filter?: types.FilterDomainInput
|
||||
): Promise<types.GetDomainsResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getDomains,
|
||||
variables: {
|
||||
projectId,
|
||||
filter
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async authenticateGitHub(
|
||||
code: string
|
||||
): Promise<types.AuthenticateGitHubResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.authenticateGitHub,
|
||||
variables: {
|
||||
code
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async unauthenticateGithub(): Promise<types.UnauthenticateGitHubResponse> {
|
||||
const { data } = await this.client.mutate({
|
||||
mutation: mutations.unauthenticateGitHub
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<types.Auction> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getAuctionData,
|
||||
variables: {
|
||||
auctionId
|
||||
}
|
||||
})
|
||||
|
||||
return data.getAuctionData
|
||||
}
|
||||
|
||||
async getDeployers(): Promise<types.GetDeployersResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getDeployers
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getAddress
|
||||
})
|
||||
|
||||
return data.address
|
||||
}
|
||||
|
||||
async verifyTx(
|
||||
txHash: string,
|
||||
amount: string,
|
||||
senderAddress: string
|
||||
): Promise<boolean> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.verifyTx,
|
||||
variables: {
|
||||
txHash,
|
||||
amount,
|
||||
senderAddress
|
||||
}
|
||||
})
|
||||
|
||||
return data.verifyTx
|
||||
}
|
||||
|
||||
async getLatestDNSRecordByProjectId(
|
||||
projectId: string
|
||||
): Promise<types.GetLatestDNSDataResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getLatestDNSRecordByProjectId,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './client'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,127 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const removeProjectMember = gql`
|
||||
mutation ($projectMemberId: String!) {
|
||||
removeProjectMember(projectMemberId: $projectMemberId)
|
||||
}
|
||||
`
|
||||
|
||||
export const updateProjectMember = gql`
|
||||
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
|
||||
updateProjectMember(projectMemberId: $projectMemberId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const addProjectMember = gql`
|
||||
mutation ($projectId: String!, $data: AddProjectMemberInput) {
|
||||
addProjectMember(projectId: $projectId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const addEnvironmentVariables = gql`
|
||||
mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) {
|
||||
addEnvironmentVariables(projectId: $projectId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const updateEnvironmentVariable = gql`
|
||||
mutation (
|
||||
$environmentVariableId: String!
|
||||
$data: UpdateEnvironmentVariableInput!
|
||||
) {
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: $environmentVariableId
|
||||
data: $data
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
export const removeEnvironmentVariable = gql`
|
||||
mutation ($environmentVariableId: String!) {
|
||||
removeEnvironmentVariable(environmentVariableId: $environmentVariableId)
|
||||
}
|
||||
`
|
||||
|
||||
export const updateDeploymentToProd = gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`
|
||||
|
||||
export const addProjectFromTemplate = gql`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) {
|
||||
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const addProject = gql`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) {
|
||||
addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateProjectMutation = gql`
|
||||
mutation ($projectId: String!, $data: UpdateProjectInput) {
|
||||
updateProject(projectId: $projectId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const updateDomainMutation = gql`
|
||||
mutation ($domainId: String!, $data: UpdateDomainInput!) {
|
||||
updateDomain(domainId: $domainId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const redeployToProd = gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
redeployToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteProject = gql`
|
||||
mutation ($projectId: String!) {
|
||||
deleteProject(projectId: $projectId)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteDomain = gql`
|
||||
mutation ($domainId: String!) {
|
||||
deleteDomain(domainId: $domainId)
|
||||
}
|
||||
`
|
||||
|
||||
export const rollbackDeployment = gql`
|
||||
mutation ($projectId: String!, $deploymentId: String!) {
|
||||
rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteDeployment = gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
deleteDeployment(deploymentId: $deploymentId)
|
||||
}
|
||||
`
|
||||
|
||||
export const addDomain = gql`
|
||||
mutation ($projectId: String!, $data: AddDomainInput!) {
|
||||
addDomain(projectId: $projectId, data: $data)
|
||||
}
|
||||
`
|
||||
|
||||
export const authenticateGitHub = gql`
|
||||
mutation ($code: String!) {
|
||||
authenticateGitHub(code: $code) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const unauthenticateGitHub = gql`
|
||||
mutation {
|
||||
unauthenticateGitHub
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,339 @@
|
||||
import { gql } from '@apollo/client'
|
||||
|
||||
export const getUser = gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
gitHubToken
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getProject = gql`
|
||||
query ($projectId: String!) {
|
||||
project(projectId: $projectId) {
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
template
|
||||
updatedAt
|
||||
prodBranch
|
||||
auctionId
|
||||
deployers {
|
||||
deployerLrn
|
||||
deployerId
|
||||
deployerApiUrl
|
||||
minimumPayment
|
||||
}
|
||||
paymentAddress
|
||||
txHash
|
||||
fundsReleased
|
||||
framework
|
||||
repository
|
||||
webhooks
|
||||
icon
|
||||
baseDomains
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
owner {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
baseDomain
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
createdAt
|
||||
environment
|
||||
applicationDeploymentRecordData {
|
||||
url
|
||||
}
|
||||
deployer {
|
||||
baseDomain
|
||||
}
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getProjectsInOrganization = gql`
|
||||
query ($organizationSlug: String!) {
|
||||
projectsInOrganization(organizationSlug: $organizationSlug) {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
auctionId
|
||||
deployers {
|
||||
deployerLrn
|
||||
deployerId
|
||||
deployerApiUrl
|
||||
minimumPayment
|
||||
}
|
||||
paymentAddress
|
||||
txHash
|
||||
fundsReleased
|
||||
prodBranch
|
||||
webhooks
|
||||
repository
|
||||
updatedAt
|
||||
icon
|
||||
baseDomains
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
baseDomain
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
commitMessage
|
||||
createdAt
|
||||
environment
|
||||
applicationDeploymentRecordData {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getOrganizations = gql`
|
||||
query {
|
||||
organizations {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getDeployments = gql`
|
||||
query ($projectId: String!) {
|
||||
deployments(projectId: $projectId) {
|
||||
id
|
||||
branch
|
||||
commitHash
|
||||
commitMessage
|
||||
url
|
||||
deployer {
|
||||
deployerLrn
|
||||
deployerId
|
||||
deployerApiUrl
|
||||
minimumPayment
|
||||
}
|
||||
environment
|
||||
isCurrent
|
||||
baseDomain
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
applicationDeploymentRequestId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getEnvironmentVariables = gql`
|
||||
query ($projectId: String!) {
|
||||
environmentVariables(projectId: $projectId) {
|
||||
createdAt
|
||||
environment
|
||||
id
|
||||
key
|
||||
updatedAt
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getProjectMembers = gql`
|
||||
query ($projectId: String!) {
|
||||
projectMembers(projectId: $projectId) {
|
||||
id
|
||||
member {
|
||||
id
|
||||
name
|
||||
email
|
||||
isVerified
|
||||
}
|
||||
isPending
|
||||
createdAt
|
||||
updatedAt
|
||||
permissions
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const searchProjects = gql`
|
||||
query ($searchText: String!) {
|
||||
searchProjects(searchText: $searchText) {
|
||||
id
|
||||
name
|
||||
prodBranch
|
||||
repository
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
auctionId
|
||||
deployers {
|
||||
deployerLrn
|
||||
deployerId
|
||||
deployerApiUrl
|
||||
minimumPayment
|
||||
}
|
||||
paymentAddress
|
||||
txHash
|
||||
fundsReleased
|
||||
prodBranch
|
||||
webhooks
|
||||
updatedAt
|
||||
template
|
||||
repository
|
||||
organization {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getDomains = gql`
|
||||
query ($projectId: String!, $filter: FilterDomainsInput) {
|
||||
domains(projectId: $projectId, filter: $filter) {
|
||||
branch
|
||||
createdAt
|
||||
redirectTo {
|
||||
id
|
||||
name
|
||||
branch
|
||||
status
|
||||
}
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getAuctionData = gql`
|
||||
query ($auctionId: String!) {
|
||||
getAuctionData(auctionId: $auctionId){
|
||||
id
|
||||
kind
|
||||
status
|
||||
ownerAddress
|
||||
createTime
|
||||
commitsEndTime
|
||||
revealsEndTime
|
||||
commitFee {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
revealFee {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
minimumBid {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
winnerAddresses
|
||||
winnerBids {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
winnerPrice {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
maxPrice {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
numProviders
|
||||
fundsReleased
|
||||
bids {
|
||||
bidderAddress
|
||||
status
|
||||
commitHash
|
||||
commitTime
|
||||
revealTime
|
||||
commitFee {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
revealFee {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
bidAmount {
|
||||
type
|
||||
quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getDeployers = gql`
|
||||
query {
|
||||
deployers {
|
||||
deployerLrn
|
||||
deployerId
|
||||
deployerApiUrl
|
||||
minimumPayment
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const getAddress = gql`
|
||||
query {
|
||||
address
|
||||
}
|
||||
`
|
||||
|
||||
export const verifyTx = gql`
|
||||
query ($txHash: String!, $amount: String!, $senderAddress: String!) {
|
||||
verifyTx(txHash: $txHash, amount: $amount, senderAddress: $senderAddress)
|
||||
}
|
||||
`
|
||||
|
||||
export const getLatestDNSRecordByProjectId = gql`
|
||||
query($projectId: String!) {
|
||||
latestDNSRecord(projectId: $projectId) {
|
||||
name
|
||||
value
|
||||
request
|
||||
resourceType
|
||||
version
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,403 @@
|
||||
// Note: equivalent to types present in GQL schema
|
||||
|
||||
export enum Role {
|
||||
Owner = 'Owner',
|
||||
Maintainer = 'Maintainer',
|
||||
Reader = 'Reader'
|
||||
}
|
||||
|
||||
export enum Permission {
|
||||
View = 'View',
|
||||
Edit = 'Edit'
|
||||
}
|
||||
|
||||
export enum Environment {
|
||||
Production = 'Production',
|
||||
Preview = 'Preview',
|
||||
Development = 'Development'
|
||||
}
|
||||
|
||||
export enum DeploymentStatus {
|
||||
Building = 'Building',
|
||||
Ready = 'Ready',
|
||||
Error = 'Error',
|
||||
Deleting = 'Deleting'
|
||||
}
|
||||
|
||||
export enum AuctionStatus {
|
||||
AuctionStatusCommitPhase = 'commit',
|
||||
AuctionStatusRevealPhase = 'reveal',
|
||||
AuctionStatusExpired = 'expired',
|
||||
AuctionStatusCompleted = 'completed'
|
||||
}
|
||||
|
||||
export type Bid = {
|
||||
auctionId: string
|
||||
bidderAddress: string
|
||||
status: string
|
||||
commitHash: string
|
||||
commitTime?: Date
|
||||
commitFee?: string
|
||||
revealTime?: Date
|
||||
revealFee?: string
|
||||
bidAmount?: string
|
||||
}
|
||||
|
||||
export type Auction = {
|
||||
id: string
|
||||
kind: string
|
||||
status: string
|
||||
ownerAddress: string
|
||||
createTime?: Date
|
||||
commitsEndTime?: Date
|
||||
revealsEndTime?: Date
|
||||
commitFee?: string
|
||||
revealFee?: string
|
||||
minimumBid?: string
|
||||
winnerAddresses: string[]
|
||||
winnerBids?: string[]
|
||||
winnerPrice?: string
|
||||
maxPrice?: string
|
||||
numProviders: number
|
||||
fundsReleased: boolean
|
||||
bids: Bid[]
|
||||
}
|
||||
|
||||
export enum DomainStatus {
|
||||
Live = 'Live',
|
||||
Pending = 'Pending'
|
||||
}
|
||||
|
||||
export type EnvironmentVariable = {
|
||||
id: string
|
||||
environment: Environment
|
||||
key: string
|
||||
value: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type Domain = {
|
||||
id: string
|
||||
branch: string
|
||||
name: string
|
||||
status: DomainStatus
|
||||
redirectTo: Domain | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
isVerified: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
gitHubToken: string | null
|
||||
}
|
||||
|
||||
export type Deployment = {
|
||||
id: string
|
||||
branch: string
|
||||
commitHash: string
|
||||
commitMessage: string
|
||||
url?: string
|
||||
deployer: Deployer
|
||||
environment: Environment
|
||||
isCurrent: boolean
|
||||
baseDomain?: string
|
||||
applicationDeploymentRecordData: AppDeploymentRecordAttributes
|
||||
status: DeploymentStatus
|
||||
createdBy: User
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
applicationDeploymentRequestId: string
|
||||
}
|
||||
|
||||
export type Deployer = {
|
||||
deployerLrn: string
|
||||
deployerId: string
|
||||
deployerApiUrl: string
|
||||
baseDomain: string
|
||||
minimumPayment: string | null
|
||||
}
|
||||
|
||||
export type OrganizationMember = {
|
||||
id: string
|
||||
member: User
|
||||
role: Role
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type ProjectMember = {
|
||||
id: string
|
||||
member: User
|
||||
permissions: Permission[]
|
||||
isPending: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type OrganizationProject = {
|
||||
id: string
|
||||
owner: User
|
||||
deployments: Deployment[]
|
||||
name: string
|
||||
repository: string
|
||||
prodBranch: string
|
||||
description: string
|
||||
template: string
|
||||
framework: string
|
||||
webhooks: string[]
|
||||
members: ProjectMember[]
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type Organization = {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
projects: OrganizationProject[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
members: OrganizationMember[]
|
||||
}
|
||||
|
||||
export type Project = {
|
||||
id: string
|
||||
owner: User
|
||||
deployments: Deployment[]
|
||||
name: string
|
||||
repository: string
|
||||
prodBranch: string
|
||||
description: string
|
||||
template: string
|
||||
framework: string
|
||||
deployers: [Deployer]
|
||||
auctionId: string
|
||||
paymentAddress: string
|
||||
txHash: string
|
||||
fundsReleased: boolean
|
||||
webhooks: string[]
|
||||
members: ProjectMember[]
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
organization: Organization
|
||||
icon: string
|
||||
baseDomains?: string[] | null
|
||||
}
|
||||
|
||||
export type GetProjectMembersResponse = {
|
||||
projectMembers: ProjectMember[]
|
||||
}
|
||||
|
||||
export type AddProjectMemberResponse = {
|
||||
addProjectMember: boolean
|
||||
}
|
||||
|
||||
export type RemoveProjectMemberResponse = {
|
||||
removeProjectMember: boolean
|
||||
}
|
||||
|
||||
export type UpdateProjectMemberResponse = {
|
||||
updateProjectMember: boolean
|
||||
}
|
||||
|
||||
export type GetDeploymentsResponse = {
|
||||
deployments: Deployment[]
|
||||
}
|
||||
|
||||
export type GetEnvironmentVariablesResponse = {
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
}
|
||||
|
||||
export type GetOrganizationsResponse = {
|
||||
organizations: Organization[]
|
||||
}
|
||||
|
||||
export type GetUserResponse = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export type GetProjectResponse = {
|
||||
project: Project | null
|
||||
}
|
||||
|
||||
export type GetProjectsInOrganizationResponse = {
|
||||
projectsInOrganization: Project[]
|
||||
}
|
||||
|
||||
export type GetDomainsResponse = {
|
||||
domains: Domain[]
|
||||
}
|
||||
|
||||
export type GetDeployersResponse = {
|
||||
deployers: Deployer[]
|
||||
}
|
||||
|
||||
export type SearchProjectsResponse = {
|
||||
searchProjects: Project[]
|
||||
}
|
||||
|
||||
export type AddEnvironmentVariablesResponse = {
|
||||
addEnvironmentVariables: boolean
|
||||
}
|
||||
|
||||
export type AddEnvironmentVariableInput = {
|
||||
environments: string[]
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type UpdateEnvironmentVariableInput = {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type UpdateProjectMemberInput = {
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
export type AddProjectMemberInput = {
|
||||
email: string
|
||||
permissions: Permission[]
|
||||
}
|
||||
|
||||
export type UpdateEnvironmentVariableResponse = {
|
||||
updateEnvironmentVariable: boolean
|
||||
}
|
||||
|
||||
export type RemoveEnvironmentVariableResponse = {
|
||||
removeEnvironmentVariable: boolean
|
||||
}
|
||||
|
||||
export type UpdateDeploymentToProdResponse = {
|
||||
updateDeploymentToProd: boolean
|
||||
}
|
||||
|
||||
export type AddProjectFromTemplateResponse = {
|
||||
addProjectFromTemplate: Project
|
||||
}
|
||||
|
||||
export type AddProjectResponse = {
|
||||
addProject: Project
|
||||
}
|
||||
|
||||
export type UpdateProjectResponse = {
|
||||
updateProject: boolean
|
||||
}
|
||||
|
||||
export type UpdateDomainResponse = {
|
||||
updateDomain: boolean
|
||||
}
|
||||
|
||||
export type DeleteProjectResponse = {
|
||||
deleteProject: boolean
|
||||
}
|
||||
|
||||
export type DeleteDomainResponse = {
|
||||
deleteDomain: boolean
|
||||
}
|
||||
|
||||
export type AddProjectFromTemplateInput = {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
owner: string
|
||||
name: string
|
||||
isPrivate: boolean
|
||||
paymentAddress: string
|
||||
txHash: string
|
||||
}
|
||||
|
||||
export type AddProjectInput = {
|
||||
name: string
|
||||
repository: string
|
||||
prodBranch: string
|
||||
template?: string
|
||||
paymentAddress: string
|
||||
txHash: string
|
||||
}
|
||||
|
||||
export type UpdateProjectInput = {
|
||||
name?: string
|
||||
description?: string
|
||||
prodBranch?: string
|
||||
webhooks?: string[]
|
||||
organizationId?: string
|
||||
}
|
||||
|
||||
export type UpdateDomainInput = {
|
||||
name?: string
|
||||
branch?: string
|
||||
redirectToId?: string | null
|
||||
}
|
||||
|
||||
export type RedeployToProdResponse = {
|
||||
redeployToProd: boolean
|
||||
}
|
||||
|
||||
export type RollbackDeploymentResponse = {
|
||||
rollbackDeployment: boolean
|
||||
}
|
||||
|
||||
export type DeleteDeploymentResponse = {
|
||||
deleteDeployment: boolean
|
||||
}
|
||||
|
||||
export type AddDomainInput = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type FilterDomainInput = {
|
||||
branch?: string
|
||||
status?: DomainStatus
|
||||
}
|
||||
|
||||
export type AddDomainResponse = {
|
||||
addDomain: true
|
||||
}
|
||||
|
||||
export type AuthenticateGitHubResponse = {
|
||||
authenticateGitHub: {
|
||||
token: string
|
||||
}
|
||||
}
|
||||
|
||||
export type UnauthenticateGitHubResponse = {
|
||||
unauthenticateGitHub: boolean
|
||||
}
|
||||
|
||||
export type AuctionParams = {
|
||||
maxPrice: string
|
||||
numProviders: number
|
||||
}
|
||||
|
||||
export type DNSRecordAttributes = {
|
||||
name: string
|
||||
value: string
|
||||
request: string
|
||||
resourceType: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export type GetLatestDNSDataResponse = {
|
||||
latestDNSRecord: DNSRecordAttributes | null
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
application: string
|
||||
auction: string
|
||||
deployer: string
|
||||
dns: string
|
||||
meta: string
|
||||
name: string
|
||||
request: string
|
||||
type: string
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsup'
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true
|
||||
})
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
@@ -19,10 +19,10 @@ function AccordionItem({
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
className={cn('border-b last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
@@ -35,8 +35,8 @@ function AccordionTrigger({
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -44,7 +44,7 @@ function AccordionTrigger({
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
@@ -58,9 +58,9 @@ function AccordionContent({
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { buttonVariants } from "@workspace/ui/components/button";
|
||||
import { buttonVariants } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
@@ -17,7 +17,7 @@ function AlertDialogTrigger({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
@@ -25,7 +25,7 @@ function AlertDialogPortal({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
@@ -36,12 +36,12 @@ function AlertDialogOverlay({
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className,
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
@@ -54,42 +54,42 @@ function AlertDialogContent({
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
@@ -99,10 +99,10 @@ function AlertDialogTitle({
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
@@ -112,10 +112,10 @@ function AlertDialogDescription({
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
@@ -127,7 +127,7 @@ function AlertDialogAction({
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
@@ -136,10 +136,10 @@ function AlertDialogCancel({
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
className={cn(buttonVariants({ variant: 'outline' }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -153,5 +153,5 @@ export {
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
AlertDialogCancel
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
"text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80",
|
||||
},
|
||||
'text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
variant: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
@@ -31,36 +31,36 @@ function Alert({
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
export { AspectRatio }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
@@ -13,12 +13,12 @@ function Avatar({
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
@@ -28,10 +28,10 @@ function AvatarImage({
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
className={cn('aspect-square size-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
@@ -42,12 +42,12 @@ function AvatarFallback({
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className,
|
||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
variant: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -40,7 +40,7 @@ function Badge({
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -1,98 +1,98 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
className={cn('inline-flex items-center gap-1.5', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
}: React.ComponentProps<'a'> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'a'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
className={cn('hover:text-foreground transition-colors', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
className={cn('text-foreground font-normal', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
}: React.ComponentProps<'li'>) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
className={cn('[&>svg]:size-3.5', className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -102,5 +102,5 @@ export {
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
BreadcrumbEllipsis
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
@@ -10,29 +10,29 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
||||
outline:
|
||||
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
@@ -40,9 +40,9 @@ function Button({
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -50,7 +50,7 @@ function Button({
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
export { Button, buttonVariants }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
import { DayPicker } from 'react-day-picker'
|
||||
|
||||
import { buttonVariants } from "@workspace/ui/components/button";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { buttonVariants } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
@@ -16,61 +16,61 @@ function Calendar({
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
className={cn('p-3', className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
months: 'flex flex-col sm:flex-row gap-2',
|
||||
month: 'flex flex-col gap-4',
|
||||
caption: 'flex justify-center pt-1 relative items-center w-full',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'flex items-center gap-1',
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100'
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
nav_button_previous: 'absolute left-1',
|
||||
nav_button_next: 'absolute right-1',
|
||||
table: 'w-full border-collapse space-x-1',
|
||||
head_row: 'flex',
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md",
|
||||
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',
|
||||
props.mode === 'range'
|
||||
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
|
||||
: '[&:has([aria-selected])]:rounded-md'
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100",
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'size-8 p-0 font-normal aria-selected:opacity-100'
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
|
||||
day_today: 'bg-accent text-accent-foreground',
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
'day-outside text-muted-foreground aria-selected:text-muted-foreground',
|
||||
day_disabled: 'text-muted-foreground opacity-50',
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
||||
day_hidden: 'invisible',
|
||||
...classNames
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
<ChevronLeft className={cn('size-4', className)} {...props} />
|
||||
),
|
||||
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
<ChevronRight className={cn('size-4', className)} {...props} />
|
||||
)
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
export { Calendar }
|
||||
|
||||
@@ -1,75 +1,68 @@
|
||||
import * as React from "react";
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className,
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn("flex flex-col gap-1.5 px-6", className)}
|
||||
className={cn('flex flex-col gap-1.5 px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6", className)}
|
||||
className={cn('flex items-center px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
type UseEmblaCarouselType
|
||||
} from 'embla-carousel-react'
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
setApi?: (api: CarouselApi) => void
|
||||
}
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
throw new Error('useCarousel must be used within a <Carousel />')
|
||||
}
|
||||
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
orientation = 'horizontal',
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
}: React.ComponentProps<'div'> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
axis: orientation === 'horizontal' ? 'x' : 'y'
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
plugins
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
if (!api) return
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}, [])
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
scrollPrev()
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
scrollNext()
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
[scrollPrev, scrollNext]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
if (!api || !setApi) return
|
||||
setApi(api)
|
||||
}, [api, setApi])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
if (!api) return
|
||||
onSelect(api)
|
||||
api.on('reInit', onSelect)
|
||||
api.on('select', onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
api?.off('select', onSelect)
|
||||
}
|
||||
}, [api, onSelect])
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
@@ -111,16 +111,16 @@ function Carousel({
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
canScrollNext
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
className={cn('relative', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
@@ -129,11 +129,11 @@ function Carousel({
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const { carouselRef, orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -143,18 +143,18 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className,
|
||||
'flex',
|
||||
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -162,22 +162,22 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className,
|
||||
'min-w-0 shrink-0 grow-0 basis-full',
|
||||
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -185,11 +185,11 @@ function CarouselPrevious({
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -left-12 -translate-y-1/2'
|
||||
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
@@ -198,16 +198,16 @@ function CarouselPrevious({
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
variant = 'outline',
|
||||
size = 'icon',
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -215,11 +215,11 @@ function CarouselNext({
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
'absolute size-8 rounded-full',
|
||||
orientation === 'horizontal'
|
||||
? 'top-1/2 -right-12 -translate-y-1/2'
|
||||
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
||||
className
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
@@ -228,7 +228,7 @@ function CarouselNext({
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -237,5 +237,5 @@ export {
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
CarouselNext
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
import * as React from 'react'
|
||||
import * as RechartsPrimitive from 'recharts'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
const THEMES = { light: '', dark: '.dark' } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
throw new Error('useChart must be used within a <ChartContainer />')
|
||||
}
|
||||
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
@@ -40,14 +40,14 @@ function ChartContainer({
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
}: React.ComponentProps<'div'> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
>['children']
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
@@ -56,7 +56,7 @@ function ChartContainer({
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -66,16 +66,16 @@ function ChartContainer({
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -89,26 +89,26 @@ ${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
.join('\n')}
|
||||
}
|
||||
`,
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
.join('\n')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
indicator = 'dot',
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
@@ -117,43 +117,43 @@ function ChartTooltipContent({
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
labelKey
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
React.ComponentProps<'div'> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: 'line' | 'dot' | 'dashed'
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || 'value'}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
!labelKey && typeof label === 'string'
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
<div className={cn('font-medium', labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
return <div className={cn('font-medium', labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
@@ -161,35 +161,35 @@ function ChartTooltipContent({
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
labelKey
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
const nestLabel = payload.length === 1 && indicator !== 'dot'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
'border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
const key = `${nameKey || item.name || item.dataKey || 'value'}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center",
|
||||
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||
indicator === 'dot' && 'items-center'
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
@@ -202,19 +202,19 @@ function ChartTooltipContent({
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
'h-2.5 w-2.5': indicator === 'dot',
|
||||
'w-1': indicator === 'line',
|
||||
'w-0 border-[1.5px] border-dashed bg-transparent':
|
||||
indicator === 'dashed',
|
||||
'my-0.5': nestLabel && indicator === 'dashed'
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
'--color-bg': indicatorColor,
|
||||
'--color-border': indicatorColor
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
@@ -222,8 +222,8 @@ function ChartTooltipContent({
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
'flex flex-1 justify-between leading-none',
|
||||
nestLabel ? 'items-end' : 'items-center'
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
@@ -241,49 +241,49 @@ function ChartTooltipContent({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
verticalAlign = 'bottom',
|
||||
nameKey
|
||||
}: React.ComponentProps<'div'> &
|
||||
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
'flex items-center justify-center gap-4',
|
||||
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const key = `${nameKey || item.dataKey || 'value'}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3'
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
@@ -292,55 +292,55 @@ function ChartLegendContent({
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
backgroundColor: item.color
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
if (typeof payload !== 'object' || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
'payload' in payload &&
|
||||
typeof payload.payload === 'object' &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key;
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
typeof payload[key as keyof typeof payload] === 'string'
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -349,5 +349,5 @@ export {
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
ChartStyle
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
@@ -14,8 +14,8 @@ function Checkbox({
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -26,7 +26,7 @@ function Checkbox({
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
export { Checkbox }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
@@ -16,7 +16,7 @@ function CollapsibleTrigger({
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
@@ -27,7 +27,7 @@ function CollapsibleContent({
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@workspace/ui/components/dialog";
|
||||
DialogTitle
|
||||
} from '@workspace/ui/components/dialog'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Command({
|
||||
className,
|
||||
@@ -21,22 +21,22 @@ function Command({
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
title = 'Command Palette',
|
||||
description = 'Search for a command to run...',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
title?: string
|
||||
description?: string
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
@@ -50,7 +50,7 @@ function CommandDialog({
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
@@ -66,13 +66,13 @@ function CommandInput({
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
@@ -83,12 +83,12 @@ function CommandList({
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className,
|
||||
'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
@@ -100,7 +100,7 @@ function CommandEmpty({
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
@@ -111,12 +111,12 @@ function CommandGroup({
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
@@ -126,10 +126,10 @@ function CommandSeparator({
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
className={cn('bg-border -mx-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
@@ -141,27 +141,27 @@ function CommandItem({
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -173,5 +173,5 @@ export {
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
CommandSeparator
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
@@ -17,7 +17,7 @@ function ContextMenuTrigger({
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
@@ -25,7 +25,7 @@ function ContextMenuGroup({
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
@@ -33,13 +33,13 @@ function ContextMenuPortal({
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
@@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
@@ -59,7 +59,7 @@ function ContextMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
@@ -67,14 +67,14 @@ function ContextMenuSubTrigger({
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
@@ -85,12 +85,12 @@ function ContextMenuSubContent({
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
@@ -102,23 +102,23 @@ function ContextMenuContent({
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
@@ -127,11 +127,11 @@ function ContextMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
@@ -145,7 +145,7 @@ function ContextMenuCheckboxItem({
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
@@ -170,7 +170,7 @@ function ContextMenuRadioItem({
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -181,7 +181,7 @@ function ContextMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
@@ -189,19 +189,19 @@ function ContextMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
'text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
@@ -211,26 +211,26 @@ function ContextMenuSeparator({
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -248,5 +248,5 @@ export {
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
ContextMenuRadioGroup
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
@@ -38,12 +38,12 @@ function DialogOverlay({
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className,
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
@@ -57,8 +57,8 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -69,30 +69,30 @@ function DialogContent({
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
@@ -102,10 +102,10 @@ function DialogTitle({
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
className={cn('text-lg leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
@@ -115,10 +115,10 @@ function DialogDescription({
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -131,5 +131,5 @@ export {
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
DialogTrigger
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
import type * as React from 'react'
|
||||
import { Drawer as DrawerPrimitive } from 'vaul'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
@@ -37,12 +37,12 @@ function DrawerOverlay({
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className,
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
@@ -56,12 +56,12 @@ function DrawerContent({
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className,
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg',
|
||||
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg',
|
||||
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -70,27 +70,27 @@ function DrawerContent({
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
@@ -100,10 +100,10 @@ function DrawerTitle({
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn('text-foreground font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
@@ -113,10 +113,10 @@ function DrawerDescription({
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -129,5 +129,5 @@ export {
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
DrawerDescription
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
@@ -17,7 +17,7 @@ function DropdownMenuPortal({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
@@ -28,7 +28,7 @@ function DropdownMenuTrigger({
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
@@ -42,13 +42,13 @@ function DropdownMenuContent({
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
@@ -56,17 +56,17 @@ function DropdownMenuGroup({
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@@ -75,11 +75,11 @@ function DropdownMenuItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@@ -105,7 +105,7 @@ function DropdownMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
@@ -116,7 +116,7 @@ function DropdownMenuRadioGroup({
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -140,7 +140,7 @@ function DropdownMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
@@ -148,19 +148,19 @@ function DropdownMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
@@ -170,32 +170,32 @@ function DropdownMenuSeparator({
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
@@ -204,22 +204,22 @@ function DropdownMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
@@ -230,12 +230,12 @@ function DropdownMenuSubContent({
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -253,5 +253,5 @@ export {
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
DropdownMenuSubContent
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState
|
||||
} from 'react-hook-form'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { Label } from "@workspace/ui/components/label";
|
||||
import { Label } from '@workspace/ui/components/label'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
const Form = FormProvider;
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
@@ -39,21 +39,21 @@ const FormField = <
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -61,52 +61,51 @@ const useFormField = () => {
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
...fieldState
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
className={cn('grid gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive-foreground", className)}
|
||||
className={cn('data-[error=true]:text-destructive-foreground', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
@@ -120,40 +119,40 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? '') : props.children
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive-foreground text-sm", className)}
|
||||
className={cn('text-destructive-foreground text-sm', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -164,5 +163,5 @@ export {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
FormField
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import type * as React from "react";
|
||||
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
@@ -16,12 +16,12 @@ function HoverCardTrigger({
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
@@ -31,12 +31,12 @@ function HoverCardContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from 'input-otp'
|
||||
import { MinusIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
@@ -15,40 +15,40 @@ function InputOTP({
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName,
|
||||
'flex items-center gap-2 has-disabled:opacity-50',
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { index: number }) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
}: React.ComponentProps<'div'> & { index: number }) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className,
|
||||
'border-input data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -59,15 +59,15 @@ function InputOTPSlot({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
|
||||
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot }
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type * as React from "react";
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
'border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Input };
|
||||
export { Input }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import type * as React from "react";
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@@ -13,12 +13,12 @@ function Label({
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className,
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Label };
|
||||
export { Label }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import * as MenubarPrimitive from '@radix-ui/react-menubar'
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
@@ -14,30 +14,30 @@ function Menubar({
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||
className,
|
||||
'bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
@@ -45,7 +45,7 @@ function MenubarRadioGroup({
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
@@ -56,17 +56,17 @@ function MenubarTrigger({
|
||||
<MenubarPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||
className,
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
align = 'start',
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
@@ -79,23 +79,23 @@ function MenubarContent({
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPortal>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
@@ -104,11 +104,11 @@ function MenubarItem({
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
@@ -122,7 +122,7 @@ function MenubarCheckboxItem({
|
||||
data-slot="menubar-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@@ -134,7 +134,7 @@ function MenubarCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
@@ -147,7 +147,7 @@ function MenubarRadioItem({
|
||||
data-slot="menubar-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -158,7 +158,7 @@ function MenubarRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
@@ -171,12 +171,12 @@ function MenubarLabel({
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
@@ -186,32 +186,32 @@ function MenubarSeparator({
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
@@ -220,22 +220,22 @@ function MenubarSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||
className,
|
||||
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
@@ -246,12 +246,12 @@ function MenubarSubContent({
|
||||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -270,5 +270,5 @@ export {
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
};
|
||||
MenubarSubContent
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
@@ -13,22 +13,22 @@ function NavigationMenu({
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
viewport?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
@@ -39,12 +39,12 @@ function NavigationMenuList({
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className,
|
||||
'group flex flex-1 list-none items-center justify-center gap-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
@@ -54,15 +54,15 @@ function NavigationMenuItem({
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
className={cn('relative', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1",
|
||||
);
|
||||
'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1'
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
@@ -72,16 +72,16 @@ function NavigationMenuTrigger({
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
{children}{' '}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
@@ -92,13 +92,13 @@ function NavigationMenuContent({
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
|
||||
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
@@ -108,19 +108,19 @@ function NavigationMenuViewport({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||
'absolute top-full left-0 isolate z-50 flex justify-center'
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
@@ -132,11 +132,11 @@ function NavigationMenuLink({
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
@@ -147,14 +147,14 @@ function NavigationMenuIndicator({
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className,
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -166,5 +166,5 @@ export {
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
navigationMenuTriggerStyle
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
import type * as React from "react";
|
||||
MoreHorizontalIcon
|
||||
} from 'lucide-react'
|
||||
import type * as React from 'react'
|
||||
|
||||
import { type Button, buttonVariants } from "@workspace/ui/components/button";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { type Button, buttonVariants } from '@workspace/ui/components/button'
|
||||
import { cn } from '@workspace/ui/lib/utils'
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
}: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />;
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">;
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'a'>
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size
|
||||
}),
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
@@ -72,13 +72,13 @@ function PaginationPrevious({
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
@@ -89,30 +89,30 @@ function PaginationNext({
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -122,5 +122,5 @@ export {
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
||||
PaginationEllipsis
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user