Set record data with repo commit hash and package.json content (#65)
* Get latest commit hash from repo when adding project * Update UI/UX * Fill registry record with data from package.json * Add package json type * Correct record data based on laconic console * Update README --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
76dfd3bb76
commit
9144d42f70
22
README.md
22
README.md
@ -77,21 +77,31 @@
|
|||||||
# 0.0.0.0:32771
|
# 0.0.0.0:32771
|
||||||
```
|
```
|
||||||
|
|
||||||
- Reserve authority for `snowball`
|
- Reserve authorities for `snowballtools` and `cerc-io`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority reserve snowball"
|
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority reserve snowballtools"
|
||||||
# {"success":true}
|
# {"success":true}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set authority bond
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority bond set snowball $BOND_ID"
|
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority reserve cerc-io"
|
||||||
# {"success":true}
|
# {"success":true}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Start the server
|
- Set authority bond for `snowballtools` and `cerc-io`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority bond set snowballtools $BOND_ID"
|
||||||
|
# {"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy exec cli "laconic cns authority bond set cerc-io $BOND_ID"
|
||||||
|
# {"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Start the server in `packages/backend`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start
|
||||||
|
@ -16,8 +16,10 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
|
"luxon": "^3.4.4",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||||
|
"octokit": "^3.1.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
|
@ -28,13 +28,13 @@ export enum DeploymentStatus {
|
|||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version:string
|
version:string
|
||||||
name: string
|
name?: string
|
||||||
description: string
|
description?: string
|
||||||
homepage: string
|
homepage?: string
|
||||||
license: string
|
license?: string
|
||||||
author: string
|
author?: string
|
||||||
repository: string,
|
repository?: string[],
|
||||||
app_version: string
|
app_version?: string
|
||||||
repository_ref: string
|
repository_ref: string
|
||||||
app_type: string
|
app_type: string
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,8 @@ export interface ApplicationDeploymentRequest {
|
|||||||
version: string
|
version: string
|
||||||
name: string
|
name: string
|
||||||
application: string
|
application: string
|
||||||
config: {
|
config: string,
|
||||||
env: {[key:string]: string}
|
meta: string
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
note: string
|
|
||||||
repository: string
|
|
||||||
repository_ref: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import assert from 'assert';
|
||||||
import { inc as semverInc } from 'semver';
|
import { inc as semverInc } from 'semver';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import { ApplicationDeploymentRequest } from './entity/Project';
|
import { ApplicationDeploymentRequest } from './entity/Project';
|
||||||
import { ApplicationRecord } from './entity/Deployment';
|
import { ApplicationRecord } from './entity/Deployment';
|
||||||
|
import { PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest';
|
const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const AUTHORITY_NAME = 'snowball';
|
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
@ -23,16 +25,21 @@ export class Registry {
|
|||||||
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord (data: { recordName: string, appType: string }): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> {
|
async createApplicationRecord ({
|
||||||
// TODO: Get record name from repo package.json name
|
packageJSON,
|
||||||
const recordName = data.recordName;
|
commitHash,
|
||||||
|
appType
|
||||||
|
}: {
|
||||||
|
packageJSON: PackageJSON
|
||||||
|
appType: string,
|
||||||
|
commitHash: string
|
||||||
|
}): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> {
|
||||||
// Use laconic-sdk to publish record
|
// Use laconic-sdk to publish record
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords({
|
||||||
type: APP_RECORD_TYPE,
|
type: APP_RECORD_TYPE,
|
||||||
name: recordName
|
name: packageJSON.name
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Get next version of record
|
// Get next version of record
|
||||||
@ -40,23 +47,21 @@ export class Registry {
|
|||||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
||||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
||||||
|
|
||||||
|
assert(nextVersion, 'Application record version not valid');
|
||||||
|
|
||||||
// Create record of type ApplicationRecord and publish
|
// Create record of type ApplicationRecord and publish
|
||||||
const applicationRecord = {
|
const applicationRecord = {
|
||||||
type: APP_RECORD_TYPE,
|
type: APP_RECORD_TYPE,
|
||||||
version: nextVersion ?? '',
|
version: nextVersion,
|
||||||
name: recordName,
|
repository_ref: commitHash,
|
||||||
|
app_type: appType,
|
||||||
// TODO: Get data from repo package.json
|
...(packageJSON.name && { name: packageJSON.name }),
|
||||||
description: '',
|
...(packageJSON.description && { description: packageJSON.description }),
|
||||||
homepage: '',
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||||
license: '',
|
...(packageJSON.license && { license: packageJSON.license }),
|
||||||
author: '',
|
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
||||||
repository: '',
|
...(packageJSON.repository && { repository: [packageJSON.repository] }),
|
||||||
app_version: '0.1.0',
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
|
|
||||||
// TODO: Get latest commit hash from repo production branch / deployment
|
|
||||||
repository_ref: '10ac6678e8372a05ad5bb1c34c34',
|
|
||||||
app_type: data.appType
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await this.registry.setRecord(
|
||||||
@ -71,8 +76,8 @@ export class Registry {
|
|||||||
|
|
||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of crn
|
// TODO: Discuss computation of CRN
|
||||||
const crn = this.getCrn(data.recordName);
|
const crn = this.getCrn(packageJSON.name ?? '');
|
||||||
|
|
||||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
||||||
@ -81,7 +86,14 @@ export class Registry {
|
|||||||
return { registryRecordId: result.data.id, registryRecordData: applicationRecord };
|
return { registryRecordId: result.data.id, registryRecordData: applicationRecord };
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: { appName: string }): Promise<{registryRecordId: string, registryRecordData: ApplicationDeploymentRequest}> {
|
async createApplicationDeploymentRequest (data: {
|
||||||
|
appName: string,
|
||||||
|
commitHash: string,
|
||||||
|
repository: string
|
||||||
|
}): Promise<{
|
||||||
|
registryRecordId: string,
|
||||||
|
registryRecordData: ApplicationDeploymentRequest
|
||||||
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
const crn = this.getCrn(data.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
@ -101,16 +113,17 @@ export class Registry {
|
|||||||
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
||||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||||
|
|
||||||
config: {
|
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||||
|
config: JSON.stringify({
|
||||||
env: {
|
env: {
|
||||||
CERC_WEBAPP_DEBUG: `${applicationRecord.attributes.app_version}`
|
CERC_WEBAPP_DEBUG: `${applicationRecord.attributes.app_version}`
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
meta: {
|
meta: JSON.stringify({
|
||||||
note: `Added by Snowball @ ${(new Date()).toISOString()}`,
|
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
||||||
repository: applicationRecord.attributes.repository,
|
repository: data.repository,
|
||||||
repository_ref: applicationRecord.attributes.repository_ref
|
repository_ref: data.commitHash
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const result = await this.registry.setRecord(
|
||||||
@ -128,7 +141,14 @@ export class Registry {
|
|||||||
return { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest };
|
return { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest };
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
getCrn (packageJsonName: string): string {
|
||||||
return `crn://${AUTHORITY_NAME}/applications/${appName}`;
|
const [arg1, arg2] = packageJsonName.split('/');
|
||||||
|
|
||||||
|
if (arg2) {
|
||||||
|
const authority = arg1.replace('@', '');
|
||||||
|
return `crn://${authority}/applications/${arg2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `crn://${arg1}/applications/${arg1}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
import { OAuthApp } from '@octokit/oauth-app';
|
import { OAuthApp } from '@octokit/oauth-app';
|
||||||
|
|
||||||
@ -18,12 +19,12 @@ const log = debug('snowball:service');
|
|||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
private app: OAuthApp;
|
private oauthApp: OAuthApp;
|
||||||
private registry: Registry;
|
private registry: Registry;
|
||||||
|
|
||||||
constructor (db: Database, app: OAuthApp, registry: Registry) {
|
constructor (db: Database, app: OAuthApp, registry: Registry) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.app = app;
|
this.oauthApp = app;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +36,13 @@ export class Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOctokit (userId: string): Promise<Octokit> {
|
||||||
|
const user = await this.db.getUser({ where: { id: userId } });
|
||||||
|
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
|
||||||
|
|
||||||
|
return new Octokit({ auth: user.gitHubToken });
|
||||||
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||||
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
@ -200,7 +208,10 @@ export class Service {
|
|||||||
await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployement = await this.createDeployment(userId,
|
const newDeployement = await this.createDeployment(userId,
|
||||||
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
isCurrent: true,
|
isCurrent: true,
|
||||||
@ -213,10 +224,28 @@ export class Service {
|
|||||||
return newDeployement;
|
return newDeployement;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDeployment (userId: string, data: DeepPartial<Deployment>): Promise<Deployment> {
|
async createDeployment (userId: string, octokit: Octokit, data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
|
const [owner, repo] = data.project.repository.split('/');
|
||||||
|
|
||||||
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'package.json',
|
||||||
|
ref: data.commitHash
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!packageJSONData) {
|
||||||
|
throw new Error('Package.json file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
|
const packageJSON = JSON.parse(atob(packageJSONData.content));
|
||||||
|
|
||||||
const { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({
|
const { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({
|
||||||
recordName: data.project?.name ?? '',
|
packageJSON,
|
||||||
appType: data.project?.template ?? ''
|
appType: data.project!.template!,
|
||||||
|
commitHash: data.commitHash!
|
||||||
});
|
});
|
||||||
|
|
||||||
const newDeployement = await this.db.addDeployement({
|
const newDeployement = await this.db.addDeployement({
|
||||||
@ -250,24 +279,43 @@ export class Service {
|
|||||||
|
|
||||||
const project = await this.db.addProject(userId, organization.id, data);
|
const project = await this.db.addProject(userId, organization.id, data);
|
||||||
|
|
||||||
// TODO: Get repository details from github
|
const octokit = await this.getOctokit(userId);
|
||||||
await this.createDeployment(userId,
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
|
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
sha: project.prodBranch,
|
||||||
|
per_page: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
|
||||||
|
// Create deployment with prod branch and latest commit
|
||||||
|
const newDeployment = await this.createDeployment(userId,
|
||||||
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
isCurrent: true,
|
isCurrent: true,
|
||||||
branch: project.prodBranch,
|
branch: project.prodBranch,
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
// TODO: Set latest commit hash
|
domain: null,
|
||||||
commitHash: '',
|
commitHash: latestCommit.sha
|
||||||
domain: null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest({ appName: project.name });
|
const { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest(
|
||||||
|
{
|
||||||
|
appName: newDeployment.registryRecordData.name!,
|
||||||
|
commitHash: latestCommit.sha,
|
||||||
|
repository: repoDetails.git_url
|
||||||
|
});
|
||||||
await this.db.updateProjectById(project.id, {
|
await this.db.updateProjectById(project.id, {
|
||||||
registryRecordId,
|
registryRecordId,
|
||||||
registryRecordData
|
registryRecordData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Setup repo webhook for push events
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +359,10 @@ export class Service {
|
|||||||
|
|
||||||
await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
|
await this.db.updateDeploymentById(deploymentId, { domain: null, isCurrent: false });
|
||||||
|
|
||||||
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployement = await this.createDeployment(userId,
|
const newDeployement = await this.createDeployment(userId,
|
||||||
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
// TODO: Put isCurrent field in project
|
// TODO: Put isCurrent field in project
|
||||||
@ -435,7 +486,7 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
||||||
const { authentication: { token } } = await this.app.createToken({
|
const { authentication: { token } } = await this.oauthApp.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
9
packages/backend/src/types.ts
Normal file
9
packages/backend/src/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface PackageJSON {
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
author?: string;
|
||||||
|
description?: string;
|
||||||
|
homepage?: string;
|
||||||
|
license?: string;
|
||||||
|
repository?: string;
|
||||||
|
}
|
20
packages/backend/test/fixtures/deployments.json
vendored
20
packages/backend/test/fixtures/deployments.json
vendored
@ -10,7 +10,7 @@
|
|||||||
"registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-ffhae3zq.snowball.xyz"
|
"url": "testProject-ffhae3zq.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -24,7 +24,7 @@
|
|||||||
"registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-vehagei8.snowball.xyz"
|
"url": "testProject-vehagei8.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -38,7 +38,7 @@
|
|||||||
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-qmgekyte.snowball.xyz"
|
"url": "testProject-qmgekyte.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,7 +52,7 @@
|
|||||||
"registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "prod",
|
"branch": "prod",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-f8wsyim6.snowball.xyz"
|
"url": "testProject-f8wsyim6.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -66,7 +66,7 @@
|
|||||||
"registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-2-eO8cckxk.snowball.xyz"
|
"url": "testProject-2-eO8cckxk.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,7 +80,7 @@
|
|||||||
"registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-2-yaq0t5yw.snowball.xyz"
|
"url": "testProject-2-yaq0t5yw.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,7 +94,7 @@
|
|||||||
"registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
"registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "testProject-2-hwwr6sbx.snowball.xyz"
|
"url": "testProject-2-hwwr6sbx.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,7 +108,7 @@
|
|||||||
"registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "iglootools-ndxje48a.snowball.xyz"
|
"url": "iglootools-ndxje48a.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -122,7 +122,7 @@
|
|||||||
"registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "iglootools-gtgpgvei.snowball.xyz"
|
"url": "iglootools-gtgpgvei.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,7 +136,7 @@
|
|||||||
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"registryRecordData": {},
|
"registryRecordData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "testXyz",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"url": "iglootools-b4bpthjr.snowball.xyz"
|
"url": "iglootools-b4bpthjr.snowball.xyz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
20
packages/backend/test/fixtures/projects.json
vendored
20
packages/backend/test/fixtures/projects.json
vendored
@ -3,10 +3,10 @@
|
|||||||
"ownerIndex": 0,
|
"ownerIndex": 0,
|
||||||
"organizationIndex": 0,
|
"organizationIndex": 0,
|
||||||
"name": "testProject",
|
"name": "testProject",
|
||||||
"repository": "test",
|
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test",
|
"description": "test",
|
||||||
"template": "test",
|
"template": "webapp",
|
||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@ -18,10 +18,10 @@
|
|||||||
"ownerIndex": 1,
|
"ownerIndex": 1,
|
||||||
"organizationIndex": 0,
|
"organizationIndex": 0,
|
||||||
"name": "testProject-2",
|
"name": "testProject-2",
|
||||||
"repository": "test-2",
|
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-2",
|
"description": "test-2",
|
||||||
"template": "test-2",
|
"template": "webapp",
|
||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@ -33,10 +33,10 @@
|
|||||||
"ownerIndex": 2,
|
"ownerIndex": 2,
|
||||||
"organizationIndex": 0,
|
"organizationIndex": 0,
|
||||||
"name": "iglootools",
|
"name": "iglootools",
|
||||||
"repository": "test-3",
|
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-3",
|
"description": "test-3",
|
||||||
"template": "test-3",
|
"template": "webapp",
|
||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@ -48,10 +48,10 @@
|
|||||||
"ownerIndex": 1,
|
"ownerIndex": 1,
|
||||||
"organizationIndex": 0,
|
"organizationIndex": 0,
|
||||||
"name": "iglootools-2",
|
"name": "iglootools-2",
|
||||||
"repository": "test-4",
|
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-4",
|
"description": "test-4",
|
||||||
"template": "test-4",
|
"template": "webapp",
|
||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@ -63,10 +63,10 @@
|
|||||||
"ownerIndex": 0,
|
"ownerIndex": 0,
|
||||||
"organizationIndex": 1,
|
"organizationIndex": 1,
|
||||||
"name": "snowball-2",
|
"name": "snowball-2",
|
||||||
"repository": "test-5",
|
"repository": "snowball-tools/snowball-ts-framework-template",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-5",
|
"description": "test-5",
|
||||||
"template": "test-5",
|
"template": "webapp",
|
||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
|
Avatar,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import { relativeTimeISO } from '../../utils/time';
|
import { relativeTimeISO } from '../../utils/time';
|
||||||
@ -20,7 +21,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
||||||
<div className="flex gap-2 p-2 items-center">
|
<div className="flex gap-2 p-2 items-center">
|
||||||
<div>{project.icon}</div>
|
<Avatar variant="square" src={project.icon} />
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Link to={`projects/${project.id}`}>
|
<Link to={`projects/${project.id}`}>
|
||||||
<Typography>{project.name}</Typography>
|
<Typography>{project.name}</Typography>
|
||||||
@ -42,10 +43,10 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray">
|
||||||
{project.latestCommit.message}
|
^ {project.latestCommit.message}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray">
|
||||||
{relativeTimeISO(project.latestCommit.createdAt)} on{' '}
|
{relativeTimeISO(project.latestCommit.createdAt)} on ^
|
||||||
{project.latestCommit.branch}
|
{project.latestCommit.branch}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
ListItemPrefix,
|
ListItemPrefix,
|
||||||
Card,
|
Card,
|
||||||
Typography,
|
Typography,
|
||||||
|
Avatar,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import SearchBar from '../SearchBar';
|
import SearchBar from '../SearchBar';
|
||||||
@ -86,7 +87,7 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
{...getItemProps({ item, index })}
|
{...getItemProps({ item, index })}
|
||||||
>
|
>
|
||||||
<ListItemPrefix>
|
<ListItemPrefix>
|
||||||
<i>^</i>
|
<Avatar src={item.icon} variant="square" />
|
||||||
</ListItemPrefix>
|
</ListItemPrefix>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h6" color="blue-gray">
|
<Typography variant="h6" color="blue-gray">
|
||||||
|
@ -32,8 +32,9 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
<div>
|
<div>
|
||||||
<p>Connect to your git account</p>
|
<p>Connect to your git account</p>
|
||||||
<p>
|
<p>
|
||||||
Once connected, you can create projects by importing repositories
|
Once connected, you can import a repository from your
|
||||||
under the account
|
<br />
|
||||||
|
account or start with one of our templates.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex">
|
<div className="mt-2 flex">
|
||||||
|
@ -22,12 +22,12 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
|||||||
<DialogBody>
|
<DialogBody>
|
||||||
In order to assign a domain to your production deployments, configure it
|
In order to assign a domain to your production deployments, configure it
|
||||||
in the{' '}
|
in the{' '}
|
||||||
{/* TODO: Navigate to settings tab panel after clicking on project settings */}
|
<Link to="../settings/domains" className="text-light-blue-800 inline">
|
||||||
<Link to="" className="text-light-blue-800 inline">
|
|
||||||
project settings{' '}
|
project settings{' '}
|
||||||
</Link>
|
</Link>
|
||||||
(recommended). If you want to assign to this specific deployment,
|
(recommended). If you want to assign to this specific deployment,
|
||||||
however, you can do so using our command-line interface:
|
however, you can do so using our command-line interface:
|
||||||
|
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
|
||||||
<CopyBlock
|
<CopyBlock
|
||||||
text="snowball alias <deployment> <domain>"
|
text="snowball alias <deployment> <domain>"
|
||||||
language=""
|
language=""
|
||||||
|
@ -18,6 +18,7 @@ import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
|||||||
import AssignDomainDialog from './AssignDomainDialog';
|
import AssignDomainDialog from './AssignDomainDialog';
|
||||||
import { DeploymentDetails } from '../../../../types/project';
|
import { DeploymentDetails } from '../../../../types/project';
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: DeploymentDetails;
|
deployment: DeploymentDetails;
|
||||||
@ -101,12 +102,13 @@ const DeploymentDetailsCard = ({
|
|||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Typography color="gray">^ {deployment.branch}</Typography>
|
<Typography color="gray">^ {deployment.branch}</Typography>
|
||||||
<Typography color="gray">
|
<Typography color="gray">
|
||||||
^ {deployment.commitHash} {deployment.commit.message}
|
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
|
{deployment.commit.message}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 flex items-center">
|
<div className="col-span-1 flex items-center">
|
||||||
<Typography color="gray" className="grow">
|
<Typography color="gray" className="grow">
|
||||||
{relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Menu placement="bottom-start">
|
<Menu placement="bottom-start">
|
||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
|
@ -4,6 +4,7 @@ import { Typography, Chip, Card } from '@material-tailwind/react';
|
|||||||
import { color } from '@material-tailwind/react/types/components/chip';
|
import { color } from '@material-tailwind/react/types/components/chip';
|
||||||
import { DeploymentDetails } from '../../../../types/project';
|
import { DeploymentDetails } from '../../../../types/project';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
|
|
||||||
interface DeploymentDialogBodyCardProps {
|
interface DeploymentDialogBodyCardProps {
|
||||||
deployment: DeploymentDetails;
|
deployment: DeploymentDetails;
|
||||||
@ -31,7 +32,8 @@ const DeploymentDialogBodyCard = ({
|
|||||||
{deployment.url}
|
{deployment.url}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
^ {deployment.branch} ^ {deployment.commitHash}{' '}
|
^ {deployment.branch} ^{' '}
|
||||||
|
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commit.message}
|
{deployment.commit.message}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
|
@ -8,3 +8,5 @@ export const ORGANIZATION_ID = '2379cf1f-a232-4ad2-ae14-4d881131cc26';
|
|||||||
|
|
||||||
export const GIT_TEMPLATE_LINK =
|
export const GIT_TEMPLATE_LINK =
|
||||||
'https://git.vdb.to/cerc-io/test-progressive-web-app';
|
'https://git.vdb.to/cerc-io/test-progressive-web-app';
|
||||||
|
|
||||||
|
export const SHORT_COMMIT_HASH_LENGTH = 8;
|
||||||
|
@ -30,9 +30,10 @@ const Import = () => {
|
|||||||
<div>^</div>
|
<div>^</div>
|
||||||
<div className="grow">{repoName}</div>
|
<div className="grow">{repoName}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-5/6 p-6">
|
||||||
<Deploy />
|
<Deploy />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Outlet, useLocation, useSearchParams } from 'react-router-dom';
|
import { Outlet, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Avatar } from '@material-tailwind/react';
|
||||||
|
|
||||||
import Stepper from '../../../../components/Stepper';
|
import Stepper from '../../../../components/Stepper';
|
||||||
import templateDetails from '../../../../assets/templates.json';
|
import templateDetails from '../../../../assets/templates.json';
|
||||||
import { GIT_TEMPLATE_LINK } from '../../../../constants';
|
import { GIT_TEMPLATE_LINK } from '../../../../constants';
|
||||||
@ -29,12 +31,12 @@ const CreateWithTemplate = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6">
|
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6 items-center">
|
||||||
<div>^</div>
|
<Avatar variant="square" />
|
||||||
<div className="grow">{template?.name}</div>
|
<div className="grow px-2">{template?.name}</div>
|
||||||
<div>
|
<div>
|
||||||
<a href={GIT_TEMPLATE_LINK} target="_blank" rel="noreferrer">
|
<a href={GIT_TEMPLATE_LINK} target="_blank" rel="noreferrer">
|
||||||
cerc-io/test-progressive-web-app
|
^ cerc-io/test-progressive-web-app
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Domain, DomainStatus } from 'gql-client';
|
import { Domain, DomainStatus } from 'gql-client';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
|
||||||
import { Typography, Button, Chip } from '@material-tailwind/react';
|
import { Typography, Button, Chip, Avatar } from '@material-tailwind/react';
|
||||||
|
|
||||||
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
@ -96,7 +96,7 @@ const OverviewTabPanel = () => {
|
|||||||
<div className="grid grid-cols-5">
|
<div className="grid grid-cols-5">
|
||||||
<div className="col-span-3 p-2">
|
<div className="col-span-3 p-2">
|
||||||
<div className="flex items-center gap-2 p-2 ">
|
<div className="flex items-center gap-2 p-2 ">
|
||||||
<div>^</div>
|
<Avatar src={project.icon} variant="square" />
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Typography>{project.name}</Typography>
|
<Typography>{project.name}</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray">
|
||||||
@ -137,7 +137,7 @@ const OverviewTabPanel = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between p-2 text-sm">
|
<div className="flex justify-between p-2 text-sm">
|
||||||
<p>^ Source</p>
|
<p>^ Source</p>
|
||||||
<p>{project.deployments[0]?.branch}</p>
|
<p>^ {project.deployments[0]?.branch}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between p-2 text-sm">
|
<div className="flex justify-between p-2 text-sm">
|
||||||
<p>^ Deployment</p>
|
<p>^ Deployment</p>
|
||||||
|
Loading…
Reference in New Issue
Block a user