Merge branch 'main' into andrehadianto/T-4912-create-project-create-repository-page
This commit is contained in:
commit
d50f559a07
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
// IntelliSense for taiwind variants
|
// IntelliSense for taiwind variants
|
||||||
"tailwindCSS.experimental.classRegex": [
|
"tailwindCSS.experimental.classRegex": [
|
||||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
"tv\\('([^)]*)\\')",
|
||||||
|
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,14 @@ import {
|
|||||||
ApplicationDeploymentRequest
|
ApplicationDeploymentRequest
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||||
|
import { sleep } from './utils';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||||
|
const SLEEP_DURATION = 1000;
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
@ -111,16 +113,21 @@ export class Registry {
|
|||||||
const crn = this.getCrn(appName);
|
const crn = this.getCrn(appName);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||||
|
|
||||||
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{ cid: result.data.id, crn },
|
{ cid: result.data.id, crn },
|
||||||
this.registryConfig.privateKey,
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
this.registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||||
this.registryConfig.privateKey,
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
this.registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await this.registry.setName(
|
||||||
{
|
{
|
||||||
cid: result.data.id,
|
cid: result.data.id,
|
||||||
@ -139,9 +146,9 @@ export class Registry {
|
|||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentRequest (data: {
|
||||||
deployment: Deployment,
|
deployment: Deployment,
|
||||||
appName: string,
|
appName: string,
|
||||||
packageJsonName: string,
|
|
||||||
repository: string,
|
repository: string,
|
||||||
environmentVariables: { [key: string]: string }
|
environmentVariables: { [key: string]: string },
|
||||||
|
dns: string,
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string;
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
@ -160,7 +167,7 @@ export class Registry {
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||||
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
dns: data.dns,
|
||||||
|
|
||||||
// TODO: Not set in test-progressive-web-app CI
|
// TODO: Not set in test-progressive-web-app CI
|
||||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||||
@ -178,6 +185,7 @@ export class Registry {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await sleep(SLEEP_DURATION);
|
||||||
const result = await this.registry.setRecord(
|
const result = await this.registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: this.registryConfig.privateKey,
|
privateKey: this.registryConfig.privateKey,
|
||||||
@ -211,11 +219,12 @@ export class Registry {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||||
return records.filter((record: AppDeploymentRecord) =>
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
deployments.some(
|
deployments.some(
|
||||||
(deployment) =>
|
(deployment) =>
|
||||||
deployment.applicationRecordId === record.attributes.application
|
deployment.applicationRecordId === record.attributes.application &&
|
||||||
|
record.attributes.url.includes(deployment.id)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -382,8 +382,7 @@ export class Service {
|
|||||||
async createDeployment (
|
async createDeployment (
|
||||||
userId: string,
|
userId: string,
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
data: DeepPartial<Deployment>,
|
data: DeepPartial<Deployment>
|
||||||
recordData: { repoUrl?: string } = {}
|
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
log(
|
log(
|
||||||
@ -407,13 +406,10 @@ export class Service {
|
|||||||
|
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
if (!recordData.repoUrl) {
|
const repoUrl = (await octokit.rest.repos.get({
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({
|
|
||||||
owner,
|
owner,
|
||||||
repo
|
repo
|
||||||
});
|
})).data.html_url;
|
||||||
recordData.repoUrl = repoDetails.html_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } =
|
const { applicationRecordId, applicationRecordData } =
|
||||||
@ -422,7 +418,7 @@ export class Service {
|
|||||||
packageJSON,
|
packageJSON,
|
||||||
appType: data.project!.template!,
|
appType: data.project!.template!,
|
||||||
commitHash: data.commitHash!,
|
commitHash: data.commitHash!,
|
||||||
repoUrl: recordData.repoUrl
|
repoUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Update previous deployment with prod branch domain
|
||||||
@ -464,11 +460,23 @@ export class Service {
|
|||||||
{
|
{
|
||||||
deployment: newDeployment,
|
deployment: newDeployment,
|
||||||
appName: repo,
|
appName: repo,
|
||||||
packageJsonName: packageJSON.name,
|
repository: repoUrl,
|
||||||
repository: recordData.repoUrl,
|
environmentVariables: environmentVariablesObj,
|
||||||
environmentVariables: environmentVariablesObj
|
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// To set project DNS
|
||||||
|
if (data.environment === Environment.Production) {
|
||||||
|
await this.registry.createApplicationDeploymentRequest(
|
||||||
|
{
|
||||||
|
deployment: newDeployment,
|
||||||
|
appName: repo,
|
||||||
|
repository: repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj,
|
||||||
|
dns: `${newDeployment.project.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
@ -498,8 +506,6 @@ export class Service {
|
|||||||
per_page: 1
|
per_page: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
// Create deployment with prod branch and latest commit
|
||||||
await this.createDeployment(user.id,
|
await this.createDeployment(user.id,
|
||||||
octokit,
|
octokit,
|
||||||
@ -510,9 +516,6 @@ export class Service {
|
|||||||
domain: null,
|
domain: null,
|
||||||
commitHash: latestCommit.sha,
|
commitHash: latestCommit.sha,
|
||||||
commitMessage: latestCommit.commit.message
|
commitMessage: latestCommit.commit.message
|
||||||
},
|
|
||||||
{
|
|
||||||
repoUrl: repoDetails.html_url
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -555,8 +558,14 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||||
const { repository, ref, head_commit: headCommit } = data;
|
const { repository, ref, head_commit: headCommit, deleted } = data;
|
||||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
|
||||||
|
if (deleted) {
|
||||||
|
log(`Branch ${ref} deleted for project ${repository.full_name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Handling GitHub push event from repository: ${repository.full_name}, branch: ${ref}`);
|
||||||
const projects = await this.db.getProjects({
|
const projects = await this.db.getProjects({
|
||||||
where: { repository: repository.full_name }
|
where: { repository: repository.full_name }
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,7 @@ export interface GitPushEventPayload {
|
|||||||
id: string;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
export interface AppDeploymentRecordAttributes {
|
||||||
|
@ -66,3 +66,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
|
|
||||||
return savedEntity;
|
return savedEntity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
@ -8,57 +8,6 @@
|
|||||||
brew install jq # if you do not have jq installed already
|
brew install jq # if you do not have jq installed already
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
|
|
||||||
|
|
||||||
- Replace variables in the following files
|
|
||||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
|
||||||
- update the name & application version numbers
|
|
||||||
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
|
|
||||||
```yml
|
|
||||||
# Example
|
|
||||||
record:
|
|
||||||
...
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- Update record version in [records/application-record.yml](records/application-record.yml)
|
|
||||||
```yml
|
|
||||||
record:
|
|
||||||
type: ApplicationRecord
|
|
||||||
version: <NEW_VERSION>
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- Update commit hash in the following places:
|
|
||||||
- [records/application-record.yml](records/application-record.yml)
|
|
||||||
```yml
|
|
||||||
record:
|
|
||||||
...
|
|
||||||
repository_ref: <COMMIT_HASH>
|
|
||||||
...
|
|
||||||
```
|
|
||||||
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
|
||||||
```yml
|
|
||||||
record:
|
|
||||||
...
|
|
||||||
meta:
|
|
||||||
...
|
|
||||||
repository_ref: <COMMIT_HASH>
|
|
||||||
```
|
|
||||||
- [deploy-frontend.sh](deploy-frontend.sh)
|
|
||||||
Also be sure to update the app version
|
|
||||||
```bash
|
|
||||||
...
|
|
||||||
RCD_APP_VERSION="<NEW_VERSION>"
|
|
||||||
REPO_REF="<COMMIT_HASH>"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- Run script to deploy app
|
- Run script to deploy app
|
||||||
```
|
```
|
||||||
./deploy-frontend.sh
|
./deploy-frontend.sh
|
||||||
|
@ -2,8 +2,8 @@ services:
|
|||||||
cns:
|
cns:
|
||||||
restEndpoint: http://console.laconic.com:1317
|
restEndpoint: http://console.laconic.com:1317
|
||||||
gqlEndpoint: http://console.laconic.com:9473/api
|
gqlEndpoint: http://console.laconic.com:9473/api
|
||||||
|
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||||
|
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
chainId: laconic_9000-1
|
chainId: laconic_9000-1
|
||||||
gas: 1200000
|
gas: 1200000
|
||||||
fees: 200000aphoton
|
fees: 200000aphoton
|
||||||
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
|
||||||
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
|
||||||
|
@ -12,18 +12,17 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
|||||||
# Current date and time for note
|
# Current date and time for note
|
||||||
CURRENT_DATE_TIME=$(date -u)
|
CURRENT_DATE_TIME=$(date -u)
|
||||||
|
|
||||||
# Increment application-record version
|
CONFIG_FILE=config.yml
|
||||||
APPLICATION_RECORD_FILE="./records/application-record.yml"
|
REGISTRY_BOND_ID="8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac"
|
||||||
if [ -f "$APPLICATION_RECORD_FILE" ]; then
|
|
||||||
# Extract current version and increment it
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
CURRENT_VERSION=$(grep 'version:' $APPLICATION_RECORD_FILE | head -1 | awk '{print $2}')
|
|
||||||
IFS='.' read -ra ADDR <<< "$CURRENT_VERSION"
|
# Get latest version from registry and increment application-record version
|
||||||
VERSION_NUMBER=${ADDR[2]}
|
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-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}')
|
||||||
NEW_VERSION_NUMBER=$((VERSION_NUMBER + 1))
|
|
||||||
NEW_APPLICATION_VERSION="${ADDR[0]}.${ADDR[1]}.$NEW_VERSION_NUMBER"
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
else
|
# Set application-record version if no previous records were found
|
||||||
# If file does not exist, start from version 0.0.1
|
NEW_APPLICATION_VERSION=0.0.1
|
||||||
NEW_APPLICATION_VERSION="0.0.1"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
# Generate application-deployment-request.yml
|
||||||
@ -62,10 +61,7 @@ EOF
|
|||||||
|
|
||||||
echo "Files generated successfully."
|
echo "Files generated successfully."
|
||||||
|
|
||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
|
||||||
|
|
||||||
RECORD_FILE=records/application-record.yml
|
RECORD_FILE=records/application-record.yml
|
||||||
CONFIG_FILE=config.yml
|
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
@ -75,8 +71,11 @@ echo $RECORD_ID
|
|||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
@ -90,6 +89,7 @@ fi
|
|||||||
|
|
||||||
RECORD_FILE=records/application-deployment-request.yml
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
|
sleep 2
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
echo "ApplicationDeploymentRequest published"
|
echo "ApplicationDeploymentRequest published"
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
|
@ -13,6 +13,6 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Thu Feb 29 02:48:23 UTC 2024
|
note: Added by Snowball @ Thursday 29 February 2024 04:36:04 PM UTC
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
repository_ref: 94a9bf88a9cf8442ffd37ad924cf9a590af69431
|
repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: 0.0.25
|
version: 0.0.1
|
||||||
repository_ref: 94a9bf88a9cf8442ffd37ad924cf9a590af69431
|
repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: snowballtools-base-frontend
|
name: snowballtools-base-frontend
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"@material-tailwind/react": "^2.1.7",
|
"@material-tailwind/react": "^2.1.7",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
@ -48,7 +49,7 @@
|
|||||||
"siwe": "^2.1.4",
|
"siwe": "^2.1.4",
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"usehooks-ts": "^2.10.0",
|
"usehooks-ts": "^2.15.1",
|
||||||
"vertical-stepper-nav": "^1.0.2",
|
"vertical-stepper-nav": "^1.0.2",
|
||||||
"viem": "^2.7.11",
|
"viem": "^2.7.11",
|
||||||
"wagmi": "^2.5.7",
|
"wagmi": "^2.5.7",
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import React from 'react';
|
import React, { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { cn } from 'utils/classnames';
|
||||||
|
|
||||||
const FormatMillisecond = ({ time }: { time: number }) => {
|
export interface FormatMilliSecondProps
|
||||||
|
extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => {
|
||||||
const formatTime = Duration.fromMillis(time)
|
const formatTime = Duration.fromMillis(time)
|
||||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||||
.toObject();
|
.toObject();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||||
|
>
|
||||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useStopwatch } from 'react-timer-hook';
|
import { useStopwatch } from 'react-timer-hook';
|
||||||
|
|
||||||
import FormatMillisecond from './FormatMilliSecond';
|
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||||
|
|
||||||
const setStopWatchOffset = (time: string) => {
|
const setStopWatchOffset = (time: string) => {
|
||||||
const providedTime = new Date(time);
|
const providedTime = new Date(time);
|
||||||
@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
|
|||||||
return currentTime;
|
return currentTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||||
|
offsetTimestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||||
const { totalSeconds } = useStopwatch({
|
const { totalSeconds } = useStopwatch({
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
offsetTimestamp: offsetTimestamp,
|
offsetTimestamp: offsetTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Stopwatch, setStopWatchOffset };
|
export { Stopwatch, setStopWatchOffset };
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Typography } from '@material-tailwind/react';
|
import { Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { DeployStep, DeployStatus } from './DeployStep';
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
const TIMEOUT_DURATION = 5000;
|
const TIMEOUT_DURATION = 5000;
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
@ -31,27 +34,27 @@ const Deploy = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-7">
|
||||||
<div className="flex justify-between mb-6">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<h4>Deployment started ...</h4>
|
<Heading as="h4" className="md:text-lg font-medium">
|
||||||
<div className="flex">
|
Deployment started ...
|
||||||
^
|
</Heading>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||||
<Stopwatch
|
<Stopwatch
|
||||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
variant="outlined"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
placeholder={''}
|
variant="tertiary"
|
||||||
|
leftIcon={<WarningIcon size={16} />}
|
||||||
>
|
>
|
||||||
^ Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
dialogTitle="Cancel deployment?"
|
dialogTitle="Cancel deployment?"
|
||||||
handleOpen={handleOpen}
|
handleOpen={handleOpen}
|
||||||
@ -66,6 +69,8 @@ const Deploy = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<DeployStep
|
<DeployStep
|
||||||
title="Building"
|
title="Building"
|
||||||
status={DeployStatus.COMPLETE}
|
status={DeployStatus.COMPLETE}
|
||||||
@ -89,6 +94,7 @@ const Deploy = () => {
|
|||||||
step="4"
|
step="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
|
||||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
import { Collapse } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import FormatMillisecond from '../../FormatMilliSecond';
|
import FormatMillisecond from '../../FormatMilliSecond';
|
||||||
import processLogs from '../../../assets/process-logs.json';
|
import processLogs from '../../../assets/process-logs.json';
|
||||||
|
import { cn } from 'utils/classnames';
|
||||||
|
import {
|
||||||
|
CheckRoundFilledIcon,
|
||||||
|
ClockOutlineIcon,
|
||||||
|
CopyIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
MinusCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { useIntersectionObserver } from 'usehooks-ts';
|
||||||
|
|
||||||
enum DeployStatus {
|
enum DeployStatus {
|
||||||
PROCESSING = 'progress',
|
PROCESSING = 'progress',
|
||||||
@ -28,61 +39,115 @@ const DeployStep = ({
|
|||||||
startTime,
|
startTime,
|
||||||
processTime,
|
processTime,
|
||||||
}: DeployStepsProps) => {
|
}: DeployStepsProps) => {
|
||||||
const [collapse, setCollapse] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||||
|
threshold: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b-2">
|
<div className="border-b border-border-separator">
|
||||||
<div className="flex justify-between p-2 gap-2">
|
{/* Collapisble trigger */}
|
||||||
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
|
|
||||||
{status === DeployStatus.PROCESSING && <div>O</div>}
|
|
||||||
{status === DeployStatus.COMPLETE && (
|
|
||||||
<div>
|
|
||||||
<button
|
<button
|
||||||
|
className={cn(
|
||||||
|
'flex justify-between w-full py-5 gap-2',
|
||||||
|
disableCollapse && 'cursor-auto',
|
||||||
|
)}
|
||||||
|
tabIndex={disableCollapse ? -1 : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollapse(!collapse);
|
if (!disableCollapse) {
|
||||||
|
setIsOpen((val) => !val);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collapse ? '-' : '+'}
|
<div className={cn('grow flex items-center gap-3')}>
|
||||||
</button>
|
{/* Icon */}
|
||||||
|
<div className="w-6 h-6 grid place-content-center">
|
||||||
|
{status === DeployStatus.NOT_STARTED && (
|
||||||
|
<div className="grid place-content-center w-6 h-6 rounded-[48px] bg-base-bg-emphasized font-mono text-xs">
|
||||||
|
{step}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="grow">{title}</div>
|
|
||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<>
|
<LoaderIcon className="animate-spin text-elements-link" />
|
||||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{status === DeployStatus.COMPLETE && (
|
{status === DeployStatus.COMPLETE && (
|
||||||
<>
|
<div className="text-controls-primary">
|
||||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
{!isOpen && <PlusIcon size={24} />}
|
||||||
</>
|
{isOpen && <MinusCircleIcon size={24} />}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Collapse open={collapse}>
|
|
||||||
<div className="p-2 text-sm text-gray-500 h-36 overflow-y-scroll">
|
{/* Title */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-left text-sm md:text-base',
|
||||||
|
status === DeployStatus.PROCESSING && 'text-elements-link',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timer */}
|
||||||
|
{status === DeployStatus.PROCESSING && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||||
|
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === DeployStatus.COMPLETE && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-4.5 h-4.5 grid place-content-center">
|
||||||
|
<CheckRoundFilledIcon
|
||||||
|
className="text-elements-success"
|
||||||
|
size={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Collapsible */}
|
||||||
|
<Collapse open={isOpen}>
|
||||||
|
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||||
|
{/* Logs */}
|
||||||
{processLogs.map((log, key) => {
|
{processLogs.map((log, key) => {
|
||||||
return (
|
return (
|
||||||
<Typography
|
<p className="font-mono" key={key}>
|
||||||
variant="small"
|
|
||||||
color="gray"
|
|
||||||
key={key}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
{log}
|
{log}
|
||||||
</Typography>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="sticky bottom-0 left-1/2 flex justify-center">
|
|
||||||
|
{/* End of logs ref used for hiding gradient overlay */}
|
||||||
|
<div ref={ref} />
|
||||||
|
|
||||||
|
{/* Overflow gradient overlay */}
|
||||||
|
{!hideGradientOverlay && (
|
||||||
|
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Copy log button */}
|
||||||
|
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||||
toast.success('Logs copied');
|
toast({
|
||||||
|
title: 'Logs copied',
|
||||||
|
variant: 'success',
|
||||||
|
id: 'logs',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
color="blue"
|
leftIcon={<CopyIcon size={16} />}
|
||||||
placeholder={''}
|
|
||||||
>
|
>
|
||||||
^ Copy log
|
Copy log
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { relativeTimeISO } from '../../../utils/time';
|
|
||||||
import { GitRepositoryDetails } from '../../../types';
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
|
||||||
import { GithubIcon, LockIcon } from 'components/shared/CustomIcon';
|
|
||||||
|
|
||||||
interface ProjectRepoCardProps {
|
|
||||||
repository: GitRepositoryDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|
||||||
const client = useGQLClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
|
|
||||||
const { orgSlug } = useParams();
|
|
||||||
|
|
||||||
const createProject = useCallback(async () => {
|
|
||||||
if (!repository) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const { addProject } = await client.addProject(orgSlug!, {
|
|
||||||
name: `${repository.owner!.login}-${repository.name}`,
|
|
||||||
prodBranch: repository.default_branch!,
|
|
||||||
repository: repository.full_name,
|
|
||||||
// TODO: Compute template from repo
|
|
||||||
template: 'webapp',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Boolean(addProject)) {
|
|
||||||
setIsLoading(false);
|
|
||||||
navigate(`import?projectId=${addProject.id}`);
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Failed to create project');
|
|
||||||
}
|
|
||||||
}, [client, repository]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
|
||||||
onClick={createProject}
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-white rounded-md justify-center items-center gap-1.5 inline-flex">
|
|
||||||
<GithubIcon />
|
|
||||||
</div>
|
|
||||||
<div className="grow">
|
|
||||||
<div>
|
|
||||||
<span className="text-black">{repository.full_name}</span>
|
|
||||||
{repository.visibility === 'private' && (
|
|
||||||
<Chip
|
|
||||||
className="normal-case inline ml-6 font-normal text-xs text-xs bg-orange-50 border border-orange-200 text-orange-600 items-center gap-1 inline-flex"
|
|
||||||
size="sm"
|
|
||||||
value="Private"
|
|
||||||
icon={<LockIcon />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<Spinner className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<IconButton size="sm" placeholder={''}>
|
|
||||||
{'>'}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectRepoCard;
|
|
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Spinner } from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { relativeTimeISO } from 'utils/time';
|
||||||
|
import { GitRepositoryDetails } from 'types';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleIcon,
|
||||||
|
GithubIcon,
|
||||||
|
LockIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
|
interface ProjectRepoCardProps {
|
||||||
|
repository: GitRepositoryDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||||
|
repository,
|
||||||
|
}) => {
|
||||||
|
const client = useGQLClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { orgSlug } = useParams();
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
|
const createProject = useCallback(async () => {
|
||||||
|
if (!repository || !orgSlug) {
|
||||||
|
return toast({
|
||||||
|
id: 'missing-repository-or-org-slug',
|
||||||
|
title: 'Repository or organization slug is missing',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { addProject } = await client.addProject(orgSlug, {
|
||||||
|
name: `${repository.owner?.login}-${repository.name}`,
|
||||||
|
prodBranch: repository.default_branch as string,
|
||||||
|
repository: repository.full_name,
|
||||||
|
// TODO: Compute template from repo
|
||||||
|
template: 'webapp',
|
||||||
|
});
|
||||||
|
if (addProject) {
|
||||||
|
navigate(`import?projectId=${addProject.id}`);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
id: 'failed-to-create-project',
|
||||||
|
title: 'Failed to create project',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
id: 'failed-to-create-project',
|
||||||
|
title: 'Failed to create project',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="group flex items-start sm:items-center gap-3 px-3 py-3 cursor-pointer rounded-xl hover:bg-base-bg-emphasized relative"
|
||||||
|
onClick={createProject}
|
||||||
|
>
|
||||||
|
{/* Icon container */}
|
||||||
|
<div className="w-10 h-10 bg-base-bg rounded-md justify-center items-center flex">
|
||||||
|
<GithubIcon />
|
||||||
|
</div>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex flex-1 gap-3 flex-wrap">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="text-elements-high-em text-sm font-medium tracking-[-0.006em]">
|
||||||
|
{repository.full_name}
|
||||||
|
</p>
|
||||||
|
<p className="text-elements-low-em text-xs">
|
||||||
|
{repository.updated_at && relativeTimeISO(repository.updated_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{repository.visibility === 'private' && (
|
||||||
|
<div className="bg-orange-50 border border-orange-200 px-2 py-1 flex items-center gap-1 rounded-lg text-xs text-orange-600 h-fit">
|
||||||
|
<LockIcon />
|
||||||
|
Private
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Right action */}
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner className="h-4 w-4 absolute right-3" />
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
size="sm"
|
||||||
|
iconOnly
|
||||||
|
className="sm:group-hover:flex hidden absolute right-3"
|
||||||
|
>
|
||||||
|
<ArrowRightCircleIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ProjectRepoCard';
|
@ -3,13 +3,17 @@ import { Octokit } from 'octokit';
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { useDebounce } from 'usehooks-ts';
|
import { useDebounce } from 'usehooks-ts';
|
||||||
|
|
||||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import SearchBar from '../../SearchBar';
|
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||||
import ProjectRepoCard from './ProjectRepoCard';
|
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
import {
|
||||||
import AsyncSelect from '../../shared/AsyncSelect';
|
ChevronGrabberHorizontal,
|
||||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
GithubIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Select, SelectOption } from 'components/shared/Select';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
|
||||||
const DEFAULT_SEARCHED_REPO = '';
|
const DEFAULT_SEARCHED_REPO = '';
|
||||||
const REPOS_PER_PAGE = 5;
|
const REPOS_PER_PAGE = 5;
|
||||||
@ -18,9 +22,9 @@ interface RepositoryListProps {
|
|||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||||
const [selectedAccount, setSelectedAccount] = useState('');
|
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||||
// TODO: Add new type for Git user when required
|
// TODO: Add new type for Git user when required
|
||||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||||
@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||||
setOrgs(orgs.data);
|
setOrgs(orgs.data);
|
||||||
setGitUser(user.data);
|
setGitUser(user.data);
|
||||||
setSelectedAccount(user.data.login);
|
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchUserAndOrgs();
|
fetchUserAndOrgs();
|
||||||
@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||||
|
|
||||||
// Check if selected account is an organization
|
// Check if selected account is an organization
|
||||||
if (selectedAccount === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
query = query + ` user:${selectedAccount}`;
|
query = query + ` user:${selectedAccount}`;
|
||||||
} else {
|
} else {
|
||||||
query = query + ` org:${selectedAccount}`;
|
query = query + ` org:${selectedAccount}`;
|
||||||
@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedAccount === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||||
per_page: REPOS_PER_PAGE,
|
per_page: REPOS_PER_PAGE,
|
||||||
affiliation: 'owner',
|
affiliation: 'owner',
|
||||||
@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOrg = orgs.find((org) => org.login === selectedAccount);
|
const selectedOrg = orgs.find(
|
||||||
|
(org) => org.login === selectedAccount.value,
|
||||||
|
);
|
||||||
assert(selectedOrg, 'Selected org not found in list');
|
assert(selectedOrg, 'Selected org not found in list');
|
||||||
|
|
||||||
const result = await octokit.rest.repos.listForOrg({
|
const result = await octokit.rest.repos.listForOrg({
|
||||||
@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
const handleResetFilters = useCallback(() => {
|
const handleResetFilters = useCallback(() => {
|
||||||
assert(gitUser, 'Git user is not available');
|
assert(gitUser, 'Git user is not available');
|
||||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||||
setSelectedAccount(gitUser.login);
|
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||||
}, [gitUser]);
|
}, [gitUser]);
|
||||||
|
|
||||||
const accounts = useMemo(() => {
|
const accounts = useMemo(() => {
|
||||||
@ -107,35 +113,52 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
return [gitUser, ...orgs];
|
return [gitUser, ...orgs];
|
||||||
}, [octokit, orgs, gitUser]);
|
}, [octokit, orgs, gitUser]);
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return accounts.map((account) => ({
|
||||||
|
label: account.login,
|
||||||
|
value: account.login,
|
||||||
|
leftIcon: <GithubIcon />,
|
||||||
|
}));
|
||||||
|
}, [accounts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<section className="space-y-3">
|
||||||
<div className="flex gap-2 mb-2 items-center">
|
{/* Dropdown and search */}
|
||||||
<div className="basis-1/3">
|
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||||
<AsyncSelect
|
<div className="lg:basis-1/3 w-full">
|
||||||
|
<Select
|
||||||
|
options={options}
|
||||||
|
placeholder="Select a repository"
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(value) => setSelectedAccount(value!)}
|
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||||
>
|
rightIcon={<ChevronGrabberHorizontal />}
|
||||||
{accounts.map((account) => (
|
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||||
<Option key={account.id} value={account.login}>
|
/>
|
||||||
<div className="flex items-center gap-2 justify-start">
|
|
||||||
<GithubIcon /> {account.login}
|
|
||||||
</div>
|
</div>
|
||||||
</Option>
|
<div className="basis-2/3 flex w-full flex-grow">
|
||||||
))}
|
<Input
|
||||||
</AsyncSelect>
|
className="w-full"
|
||||||
</div>
|
|
||||||
<div className="basis-2/3 flex-grow flex items-center">
|
|
||||||
<SearchBar
|
|
||||||
value={searchedRepo}
|
value={searchedRepo}
|
||||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
|
||||||
placeholder="Search for repository"
|
placeholder="Search for repository"
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
|
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Repository list */}
|
||||||
{Boolean(repositoryDetails.length) ? (
|
{Boolean(repositoryDetails.length) ? (
|
||||||
repositoryDetails.map((repo, key) => {
|
<div className="flex flex-col gap-2">
|
||||||
return <ProjectRepoCard repository={repo} key={key} />;
|
{repositoryDetails.map((repo, index) => (
|
||||||
})
|
<>
|
||||||
|
<ProjectRepoCard repository={repo} key={index} />
|
||||||
|
{/* Horizontal line */}
|
||||||
|
{index !== repositoryDetails.length - 1 && (
|
||||||
|
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 p-6 flex items-center justify-center">
|
<div className="mt-4 p-6 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepositoryList;
|
|
@ -0,0 +1 @@
|
|||||||
|
export * from './RepositoryList';
|
@ -55,9 +55,9 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative',
|
'flex items-center gap-3 px-3 py-3 bg-base-bg-alternate hover:bg-base-bg-emphasized rounded-2xl group relative cursor-pointer',
|
||||||
{
|
{
|
||||||
'cursor-default': template?.isComingSoon,
|
'cursor-default': template?.isComingSoon,
|
||||||
},
|
},
|
||||||
@ -86,6 +86,6 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
|
|||||||
<ArrowRightCircleIcon />
|
<ArrowRightCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,31 +1,28 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import {
|
import {
|
||||||
|
Deployment,
|
||||||
|
DeploymentStatus,
|
||||||
|
Domain,
|
||||||
Environment,
|
Environment,
|
||||||
Project,
|
Project,
|
||||||
Domain,
|
|
||||||
DeploymentStatus,
|
|
||||||
Deployment,
|
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import {
|
import {
|
||||||
Menu,
|
BranchStrokeIcon,
|
||||||
MenuHandler,
|
CheckRoundFilledIcon,
|
||||||
MenuList,
|
ClockOutlineIcon,
|
||||||
MenuItem,
|
CommitIcon,
|
||||||
Typography,
|
LoadingIcon,
|
||||||
Chip,
|
WarningIcon,
|
||||||
ChipProps,
|
} from 'components/shared/CustomIcon';
|
||||||
Tooltip,
|
import { Heading } from 'components/shared/Heading';
|
||||||
} from '@material-tailwind/react';
|
import { OverflownText } from 'components/shared/OverflownText';
|
||||||
|
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import { relativeTimeMs } from 'utils/time';
|
||||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
|
||||||
import AssignDomainDialog from './AssignDomainDialog';
|
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
import { formatAddress } from '../../../../utils/format';
|
||||||
|
import { DeploymentMenu } from './DeploymentMenu';
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@ -35,10 +32,12 @@ interface DeployDetailsCardProps {
|
|||||||
prodBranchDomains: Domain[];
|
prodBranchDomains: Domain[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
|
const STATUS_COLORS: {
|
||||||
[DeploymentStatus.Building]: 'blue',
|
[key in DeploymentStatus]: TagTheme['type'];
|
||||||
[DeploymentStatus.Ready]: 'green',
|
} = {
|
||||||
[DeploymentStatus.Error]: 'red',
|
[DeploymentStatus.Building]: 'emphasized',
|
||||||
|
[DeploymentStatus.Ready]: 'positive',
|
||||||
|
[DeploymentStatus.Error]: 'negative',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeploymentDetailsCard = ({
|
const DeploymentDetailsCard = ({
|
||||||
@ -48,241 +47,99 @@ const DeploymentDetailsCard = ({
|
|||||||
project,
|
project,
|
||||||
prodBranchDomains,
|
prodBranchDomains,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
const client = useGQLClient();
|
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||||
|
if (status === DeploymentStatus.Building) {
|
||||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
return <LoadingIcon className="animate-spin" />;
|
||||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
|
||||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
|
||||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
|
||||||
|
|
||||||
const updateDeployment = async () => {
|
|
||||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
|
||||||
if (isUpdated) {
|
|
||||||
await onUpdate();
|
|
||||||
toast.success('Deployment changed to production');
|
|
||||||
} else {
|
|
||||||
toast.error('Unable to change deployment to production');
|
|
||||||
}
|
}
|
||||||
};
|
if (status === DeploymentStatus.Ready) {
|
||||||
|
return <CheckRoundFilledIcon />;
|
||||||
const redeployToProd = async () => {
|
|
||||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
|
||||||
if (isRedeployed) {
|
|
||||||
await onUpdate();
|
|
||||||
toast.success('Redeployed to production');
|
|
||||||
} else {
|
|
||||||
toast.error('Unable to redeploy to production');
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const rollbackDeploymentHandler = async () => {
|
if (status === DeploymentStatus.Error) {
|
||||||
const isRollbacked = await client.rollbackDeployment(
|
return <WarningIcon />;
|
||||||
project.id,
|
|
||||||
deployment.id,
|
|
||||||
);
|
|
||||||
if (isRollbacked) {
|
|
||||||
await onUpdate();
|
|
||||||
toast.success('Deployment rolled back');
|
|
||||||
} else {
|
|
||||||
toast.error('Unable to rollback deployment');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
<div className="flex lg:flex gap-2 lg:gap-2 2xl:gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent ">
|
||||||
<div className="col-span-3">
|
<div className="flex-1 max-w-[30%] space-y-2">
|
||||||
<div className="flex">
|
{/* DEPLOYMENT URL */}
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
<Typography className="basis-3/4" placeholder={''}>
|
<Heading
|
||||||
|
className="text-sm font-medium text-elements-high-em tracking-tight"
|
||||||
|
as="h2"
|
||||||
|
>
|
||||||
|
<OverflownText content={deployment.url}>
|
||||||
{deployment.url}
|
{deployment.url}
|
||||||
</Typography>
|
</OverflownText>
|
||||||
|
</Heading>
|
||||||
)}
|
)}
|
||||||
</div>
|
<span className="text-sm text-elements-low-em tracking-tight">
|
||||||
<Typography color="gray" placeholder={''}>
|
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||||
: 'Preview'}
|
: 'Preview'}
|
||||||
</Typography>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
|
||||||
<Chip
|
{/* DEPLOYMENT STATUS */}
|
||||||
value={deployment.status}
|
<div className="w-[10%] max-w-[110px]">
|
||||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
<Tag
|
||||||
variant="ghost"
|
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||||
icon={<i>^</i>}
|
size="xs"
|
||||||
/>
|
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||||
|
>
|
||||||
|
{deployment.status}
|
||||||
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
|
||||||
<Typography color="gray" placeholder={''}>
|
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||||
^ {deployment.branch}
|
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||||
</Typography>
|
<span className="flex gap-1.5 items-center">
|
||||||
<Typography color="gray" placeholder={''}>
|
<BranchStrokeIcon className="h-4 w-4" />
|
||||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
{deployment.branch}
|
||||||
|
</span>
|
||||||
|
<span className="flex gap-2 items-center">
|
||||||
|
<CommitIcon />
|
||||||
|
<OverflownText content={deployment.commitMessage}>
|
||||||
|
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commitMessage}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</OverflownText>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center">
|
|
||||||
<Typography color="gray" className="grow" placeholder={''}>
|
{/* DEPLOYMENT INFOs */}
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
<div className="ml-auto max-w-[312px] w-[30%] gap-1 2xl:gap-5 flex items-center justify-between text-elements-low-em text-sm">
|
||||||
<Tooltip content={deployment.createdBy.name}>
|
<div className="flex w-[70%] items-center gap-0.5 2xl:gap-2 flex-1">
|
||||||
|
<ClockOutlineIcon className="h-4 w-4" />
|
||||||
|
<OverflownText content={relativeTimeMs(deployment.createdAt) ?? ''}>
|
||||||
|
{relativeTimeMs(deployment.createdAt)}
|
||||||
|
</OverflownText>
|
||||||
|
<div>
|
||||||
|
<Avatar
|
||||||
|
type="orange"
|
||||||
|
initials={getInitials(deployment.createdBy.name ?? '')}
|
||||||
|
className="lg:size-5 2xl:size-6"
|
||||||
|
// TODO: Add avatarUrl
|
||||||
|
// imageSrc={deployment.createdBy.avatarUrl}
|
||||||
|
></Avatar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OverflownText
|
||||||
|
// className="min-w-[200px]"
|
||||||
|
content={formatAddress(deployment.createdBy?.name ?? '')}
|
||||||
|
>
|
||||||
{formatAddress(deployment.createdBy.name ?? '')}
|
{formatAddress(deployment.createdBy.name ?? '')}
|
||||||
</Tooltip>
|
</OverflownText>
|
||||||
</Typography>
|
|
||||||
<Menu placement="bottom-start">
|
|
||||||
<MenuHandler>
|
|
||||||
<button className="self-start">...</button>
|
|
||||||
</MenuHandler>
|
|
||||||
<MenuList placeholder={''}>
|
|
||||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
|
||||||
<MenuItem disabled={!Boolean(deployment.url)} placeholder={''}>
|
|
||||||
^ Visit
|
|
||||||
</MenuItem>
|
|
||||||
</a>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
^ Assign domain
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
|
||||||
disabled={!(deployment.environment !== Environment.Production)}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
^ Change to production
|
|
||||||
</MenuItem>
|
|
||||||
<hr className="my-3" />
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
|
||||||
disabled={
|
|
||||||
!(
|
|
||||||
deployment.environment === Environment.Production &&
|
|
||||||
deployment.isCurrent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
^ Redeploy to production
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
|
||||||
disabled={
|
|
||||||
deployment.isCurrent ||
|
|
||||||
deployment.environment !== Environment.Production ||
|
|
||||||
!Boolean(currentDeployment)
|
|
||||||
}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
^ Rollback to this version
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
</div>
|
||||||
<ConfirmDialog
|
<DeploymentMenu
|
||||||
dialogTitle="Change to production?"
|
className="ml-auto"
|
||||||
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
|
||||||
open={changeToProduction}
|
|
||||||
confirmButtonTitle="Change"
|
|
||||||
color="blue"
|
|
||||||
handleConfirm={async () => {
|
|
||||||
await updateDeployment();
|
|
||||||
setChangeToProduction((preVal) => !preVal);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
Upon confirmation, this deployment will be changed to production.
|
|
||||||
</Typography>
|
|
||||||
<DeploymentDialogBodyCard deployment={deployment} />
|
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
The new deployment will be associated with these domains:
|
|
||||||
</Typography>
|
|
||||||
{prodBranchDomains.length > 0 &&
|
|
||||||
prodBranchDomains.map((value) => {
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
variant="small"
|
|
||||||
color="blue"
|
|
||||||
key={value.id}
|
|
||||||
placeholder={''}
|
|
||||||
>
|
|
||||||
^ {value.name}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ConfirmDialog>
|
|
||||||
<ConfirmDialog
|
|
||||||
dialogTitle="Redeploy to production?"
|
|
||||||
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
|
||||||
open={redeployToProduction}
|
|
||||||
confirmButtonTitle="Redeploy"
|
|
||||||
color="blue"
|
|
||||||
handleConfirm={async () => {
|
|
||||||
await redeployToProd();
|
|
||||||
setRedeployToProduction((preVal) => !preVal);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
Upon confirmation, new deployment will be created with the same
|
|
||||||
source code as current deployment.
|
|
||||||
</Typography>
|
|
||||||
<DeploymentDialogBodyCard deployment={deployment} />
|
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
These domains will point to your new deployment:
|
|
||||||
</Typography>
|
|
||||||
{deployment.domain?.name && (
|
|
||||||
<Typography variant="small" color="blue" placeholder={''}>
|
|
||||||
{deployment.domain?.name}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ConfirmDialog>
|
|
||||||
{Boolean(currentDeployment) && (
|
|
||||||
<ConfirmDialog
|
|
||||||
dialogTitle="Rollback to this deployment?"
|
|
||||||
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
|
||||||
open={rollbackDeployment}
|
|
||||||
confirmButtonTitle="Rollback"
|
|
||||||
color="blue"
|
|
||||||
handleConfirm={async () => {
|
|
||||||
await rollbackDeploymentHandler();
|
|
||||||
setRollbackDeployment((preVal) => !preVal);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
Upon confirmation, this deployment will replace your current
|
|
||||||
deployment
|
|
||||||
</Typography>
|
|
||||||
<DeploymentDialogBodyCard
|
|
||||||
deployment={currentDeployment}
|
|
||||||
chip={{
|
|
||||||
value: 'Live Deployment',
|
|
||||||
color: 'green',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DeploymentDialogBodyCard
|
|
||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
chip={{
|
currentDeployment={currentDeployment}
|
||||||
value: 'New Deployment',
|
onUpdate={onUpdate}
|
||||||
color: 'orange',
|
project={project}
|
||||||
}}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small" placeholder={''}>
|
|
||||||
These domains will point to your new deployment:
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" color="blue" placeholder={''}>
|
|
||||||
^ {currentDeployment.domain?.name}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
</ConfirmDialog>
|
|
||||||
)}
|
|
||||||
<AssignDomainDialog
|
|
||||||
open={assignDomainDialog}
|
|
||||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,268 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import {
|
||||||
|
GlobeIcon,
|
||||||
|
HorizontalDotIcon,
|
||||||
|
LinkIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
RocketIcon,
|
||||||
|
UndoIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@material-tailwind/react';
|
||||||
|
import { ComponentPropsWithRef } from 'react';
|
||||||
|
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||||
|
import AssignDomainDialog from './AssignDomainDialog';
|
||||||
|
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||||
|
import { Typography } from '@material-tailwind/react';
|
||||||
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
import { cn } from 'utils/classnames';
|
||||||
|
|
||||||
|
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||||
|
deployment: Deployment;
|
||||||
|
currentDeployment: Deployment;
|
||||||
|
onUpdate: () => Promise<void>;
|
||||||
|
project: Project;
|
||||||
|
prodBranchDomains: Domain[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeploymentMenu = ({
|
||||||
|
deployment,
|
||||||
|
currentDeployment,
|
||||||
|
onUpdate,
|
||||||
|
project,
|
||||||
|
prodBranchDomains,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DeploymentMenuProps) => {
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||||
|
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||||
|
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||||
|
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||||
|
|
||||||
|
const updateDeployment = async () => {
|
||||||
|
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||||
|
if (isUpdated) {
|
||||||
|
await onUpdate();
|
||||||
|
toast.success('Deployment changed to production');
|
||||||
|
} else {
|
||||||
|
toast.error('Unable to change deployment to production');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const redeployToProd = async () => {
|
||||||
|
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||||
|
if (isRedeployed) {
|
||||||
|
await onUpdate();
|
||||||
|
toast.success('Redeployed to production');
|
||||||
|
} else {
|
||||||
|
toast.error('Unable to redeploy to production');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rollbackDeploymentHandler = async () => {
|
||||||
|
const isRollbacked = await client.rollbackDeployment(
|
||||||
|
project.id,
|
||||||
|
deployment.id,
|
||||||
|
);
|
||||||
|
if (isRollbacked) {
|
||||||
|
await onUpdate();
|
||||||
|
toast.success('Deployment rolled back');
|
||||||
|
} else {
|
||||||
|
toast.error('Unable to rollback deployment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn('max-w-[32px]', className)} {...props}>
|
||||||
|
<Menu placement="bottom-start">
|
||||||
|
<MenuHandler>
|
||||||
|
<Button
|
||||||
|
shape="default"
|
||||||
|
size="xs"
|
||||||
|
variant="unstyled"
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 rounded-full border border-transparent transition-colors background-transparent',
|
||||||
|
'[&[aria-expanded=true]]:border [&[aria-expanded=true]]:border-border-interactive [&[aria-expanded=true]]:bg-controls-tertiary [&[aria-expanded=true]]:shadow-button',
|
||||||
|
)}
|
||||||
|
leftIcon={<HorizontalDotIcon />}
|
||||||
|
aria-label="Toggle Menu"
|
||||||
|
/>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList className="text-elements-high-em" placeholder={''}>
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized"
|
||||||
|
disabled={!Boolean(deployment.url)}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
href={deployment.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<LinkIcon /> Visit
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
|
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
<GlobeIcon /> Assign domain
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
|
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||||
|
disabled={!(deployment.environment !== Environment.Production)}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
<RocketIcon /> Change to production
|
||||||
|
</MenuItem>
|
||||||
|
<hr className="my-3" />
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
|
onClick={() => setRedeployToProduction(!redeployToProduction)}
|
||||||
|
disabled={
|
||||||
|
!(
|
||||||
|
deployment.environment === Environment.Production &&
|
||||||
|
deployment.isCurrent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
<RefreshIcon /> Redeploy to production
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
|
onClick={() => setRollbackDeployment(!rollbackDeployment)}
|
||||||
|
disabled={
|
||||||
|
deployment.isCurrent ||
|
||||||
|
deployment.environment !== Environment.Production ||
|
||||||
|
!Boolean(currentDeployment)
|
||||||
|
}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
<UndoIcon /> Rollback to this version
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
{/* Dialogs */}
|
||||||
|
<ConfirmDialog
|
||||||
|
dialogTitle="Change to production?"
|
||||||
|
handleOpen={() => setChangeToProduction((preVal) => !preVal)}
|
||||||
|
open={changeToProduction}
|
||||||
|
confirmButtonTitle="Change"
|
||||||
|
color="blue"
|
||||||
|
handleConfirm={async () => {
|
||||||
|
await updateDeployment();
|
||||||
|
setChangeToProduction((preVal) => !preVal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Upon confirmation, this deployment will be changed to production.
|
||||||
|
</Typography>
|
||||||
|
<DeploymentDialogBodyCard deployment={deployment} />
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
The new deployment will be associated with these domains:
|
||||||
|
</Typography>
|
||||||
|
{prodBranchDomains.length > 0 &&
|
||||||
|
prodBranchDomains.map((value) => {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue"
|
||||||
|
key={value.id}
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
|
^ {value.name}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
dialogTitle="Redeploy to production?"
|
||||||
|
handleOpen={() => setRedeployToProduction((preVal) => !preVal)}
|
||||||
|
open={redeployToProduction}
|
||||||
|
confirmButtonTitle="Redeploy"
|
||||||
|
color="blue"
|
||||||
|
handleConfirm={async () => {
|
||||||
|
await redeployToProd();
|
||||||
|
setRedeployToProduction((preVal) => !preVal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Upon confirmation, new deployment will be created with the same
|
||||||
|
source code as current deployment.
|
||||||
|
</Typography>
|
||||||
|
<DeploymentDialogBodyCard deployment={deployment} />
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
These domains will point to your new deployment:
|
||||||
|
</Typography>
|
||||||
|
{deployment.domain?.name && (
|
||||||
|
<Typography variant="small" color="blue" placeholder={''}>
|
||||||
|
{deployment.domain?.name}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
|
{Boolean(currentDeployment) && (
|
||||||
|
<ConfirmDialog
|
||||||
|
dialogTitle="Rollback to this deployment?"
|
||||||
|
handleOpen={() => setRollbackDeployment((preVal) => !preVal)}
|
||||||
|
open={rollbackDeployment}
|
||||||
|
confirmButtonTitle="Rollback"
|
||||||
|
color="blue"
|
||||||
|
handleConfirm={async () => {
|
||||||
|
await rollbackDeploymentHandler();
|
||||||
|
setRollbackDeployment((preVal) => !preVal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
Upon confirmation, this deployment will replace your current
|
||||||
|
deployment
|
||||||
|
</Typography>
|
||||||
|
<DeploymentDialogBodyCard
|
||||||
|
deployment={currentDeployment}
|
||||||
|
chip={{
|
||||||
|
value: 'Live Deployment',
|
||||||
|
color: 'green',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DeploymentDialogBodyCard
|
||||||
|
deployment={deployment}
|
||||||
|
chip={{
|
||||||
|
value: 'New Deployment',
|
||||||
|
color: 'orange',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="small" placeholder={''}>
|
||||||
|
These domains will point to your new deployment:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" color="blue" placeholder={''}>
|
||||||
|
^ {currentDeployment.domain?.name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</ConfirmDialog>
|
||||||
|
)}
|
||||||
|
<AssignDomainDialog
|
||||||
|
open={assignDomainDialog}
|
||||||
|
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -44,7 +44,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
|
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600">
|
||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="Search branches"
|
placeholder="Search branches"
|
||||||
|
@ -11,7 +11,7 @@ export const Activity = ({
|
|||||||
activities: GitCommitWithBranch[];
|
activities: GitCommitWithBranch[];
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-2 mr-1">
|
<div className="col-span-5 md:col-span-2 mr-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||||
<Button variant="tertiary" size="sm">
|
<Button variant="tertiary" size="sm">
|
||||||
|
@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
|||||||
|
|
||||||
export const avatarTheme = tv(
|
export const avatarTheme = tv(
|
||||||
{
|
{
|
||||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||||
slots: {
|
slots: {
|
||||||
image: [
|
image: [
|
||||||
'h-full',
|
'h-full',
|
||||||
|
@ -3,17 +3,11 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
|||||||
|
|
||||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||||
return (
|
return (
|
||||||
<CustomIcon
|
<CustomIcon width="20" height="20" viewBox="0 0 20 20" {...props}>
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
d="M10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0ZM13.774 8.13327C14.1237 7.70582 14.0607 7.0758 13.6332 6.72607C13.2058 6.37635 12.5758 6.43935 12.226 6.86679L8.42576 11.5116L7.20711 10.2929C6.81658 9.9024 6.18342 9.9024 5.79289 10.2929C5.40237 10.6834 5.40237 11.3166 5.79289 11.7071L7.79289 13.7071C7.99267 13.9069 8.26764 14.0129 8.54981 13.9988C8.83199 13.9847 9.09505 13.8519 9.27396 13.6333L13.774 8.13327Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</CustomIcon>
|
</CustomIcon>
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const CommitIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_755_4058)">
|
||||||
|
<path
|
||||||
|
d="M15.5 7.50001H11.4637C11.343 6.66752 10.9264 5.90636 10.2904 5.35589C9.65427 4.80541 8.84121 4.50244 8 4.50244C7.15879 4.50244 6.34573 4.80541 5.70964 5.35589C5.07355 5.90636 4.65701 6.66752 4.53625 7.50001H0.5C0.367392 7.50001 0.240215 7.55269 0.146447 7.64646C0.0526784 7.74023 0 7.8674 0 8.00001C0 8.13262 0.0526784 8.2598 0.146447 8.35357C0.240215 8.44733 0.367392 8.50001 0.5 8.50001H4.53625C4.65701 9.33251 5.07355 10.0937 5.70964 10.6441C6.34573 11.1946 7.15879 11.4976 8 11.4976C8.84121 11.4976 9.65427 11.1946 10.2904 10.6441C10.9264 10.0937 11.343 9.33251 11.4637 8.50001H15.5C15.6326 8.50001 15.7598 8.44733 15.8536 8.35357C15.9473 8.2598 16 8.13262 16 8.00001C16 7.8674 15.9473 7.74023 15.8536 7.64646C15.7598 7.55269 15.6326 7.50001 15.5 7.50001ZM8 10.5C7.50555 10.5 7.0222 10.3534 6.61107 10.0787C6.19995 9.80398 5.87952 9.41354 5.6903 8.95672C5.50108 8.49991 5.45157 7.99724 5.54804 7.51229C5.6445 7.02733 5.8826 6.58188 6.23223 6.23224C6.58186 5.88261 7.02732 5.64451 7.51227 5.54805C7.99723 5.45158 8.49989 5.50109 8.95671 5.69031C9.41352 5.87953 9.80397 6.19996 10.0787 6.61109C10.3534 7.02221 10.5 7.50556 10.5 8.00001C10.5 8.66305 10.2366 9.29894 9.76777 9.76778C9.29893 10.2366 8.66304 10.5 8 10.5Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_755_4058">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const CopyIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M1.33301 2.66665C1.33301 1.93027 1.92996 1.33331 2.66634 1.33331H9.33301C10.0694 1.33331 10.6663 1.93027 10.6663 2.66665V5.33331H13.333C14.0694 5.33331 14.6663 5.93027 14.6663 6.66665V13.3333C14.6663 14.0697 14.0694 14.6666 13.333 14.6666H6.66634C5.92996 14.6666 5.33301 14.0697 5.33301 13.3333V10.6666H2.66634C1.92996 10.6666 1.33301 10.0697 1.33301 9.33331V2.66665ZM9.66634 5.33331H6.66634C5.92996 5.33331 5.33301 5.93027 5.33301 6.66665V9.66665H2.66634C2.48225 9.66665 2.33301 9.51741 2.33301 9.33331V2.66665C2.33301 2.48255 2.48225 2.33331 2.66634 2.33331H9.33301C9.5171 2.33331 9.66634 2.48255 9.66634 2.66665V5.33331Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const LoaderIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="25"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 25 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.5003 3V6M12.5003 18V21M6.13634 5.63604L8.25766 7.75736M16.7429 16.2426L18.8643 18.364M3.5 12.0007H6.5M18.5 12.0007H21.5M6.13634 18.364L8.25766 16.2426M16.7429 7.75736L18.8643 5.63604"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const MinusCircleIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="25"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 25 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2.5 12C2.5 6.47715 6.97715 2 12.5 2C18.0228 2 22.5 6.47715 22.5 12C22.5 17.5228 18.0228 22 12.5 22C6.97715 22 2.5 17.5228 2.5 12ZM16.5 12.9999C17.0523 12.9999 17.5 12.5522 17.5 11.9999C17.5 11.4476 17.0523 10.9999 16.5 10.9999L8.49997 11.0001C7.94769 11.0001 7.49998 11.4479 7.5 12.0001C7.50002 12.5524 7.94774 13.0001 8.50003 13.0001L16.5 12.9999Z"
|
||||||
|
fill="#0F86F5"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const RefreshIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.125 10.875V15.375H2.625M6.75 14.9666C4.33948 14.0571 2.625 11.7288 2.625 9C2.625 5.47918 5.47918 2.625 9 2.625C9.93578 2.625 10.8245 2.82663 11.625 3.1888"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.3125 15.1877C10.3125 15.4984 10.0607 15.7502 9.75 15.7502C9.43934 15.7502 9.1875 15.4984 9.1875 15.1877C9.1875 14.8771 9.43934 14.6252 9.75 14.6252C10.0607 14.6252 10.3125 14.8771 10.3125 15.1877Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.2304 13.7025C13.3857 13.9716 13.2936 14.3156 13.0245 14.4709C12.7555 14.6262 12.4115 14.5341 12.2561 14.265C12.1008 13.996 12.193 13.652 12.462 13.4966C12.7311 13.3413 13.0751 13.4335 13.2304 13.7025Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.0147 10.9573C15.2838 11.1126 15.3759 11.4567 15.2206 11.7257C15.0653 11.9947 14.7213 12.0869 14.4522 11.9316C14.1832 11.7763 14.091 11.4322 14.2463 11.1632C14.4017 10.8942 14.7457 10.802 15.0147 10.9573Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.2657 5.74407C13.9967 5.8994 13.6527 5.80722 13.4973 5.53818C13.342 5.26914 13.4342 4.92512 13.7032 4.76979C13.9723 4.61446 14.3163 4.70664 14.4716 4.97568C14.6269 5.24472 14.5348 5.58874 14.2657 5.74407Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15.75 8.25023C15.75 8.56089 15.4982 8.81273 15.1875 8.81273C14.8768 8.81273 14.625 8.56089 14.625 8.25023C14.625 7.93957 14.8768 7.68773 15.1875 7.68773C15.4982 7.68773 15.75 7.93957 15.75 8.25023Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const RocketIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.87626 10.1252H3.34633C2.77976 10.1252 2.41778 9.52109 2.68496 9.02148L4.07738 6.41779C4.33833 5.92983 4.84675 5.62517 5.40011 5.62517H8.40751M4.87626 10.1252L7.87626 13.1252M4.87626 10.1252L8.40751 5.62517M7.87626 13.1252V14.6551C7.87626 15.2217 8.48034 15.5836 8.97995 15.3165L11.5836 13.924C12.0716 13.6631 12.3763 13.1547 12.3763 12.6013V9.66486M7.87626 13.1252L12.3763 9.66486M12.3763 9.66486C14.5604 7.66276 15.8988 5.43328 16.0998 2.62499C16.1294 2.21183 15.7895 1.87198 15.3764 1.90172C12.5784 2.10308 10.4093 3.4414 8.40751 5.62517M3.37626 16.1252H1.875V14.6258C1.875 13.797 2.54717 13.1252 3.37594 13.1252C4.20437 13.1252 4.87626 13.7967 4.87626 14.6252C4.87626 15.4536 4.20469 16.1252 3.37626 16.1252Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const UndoIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.49989 3.75L2.03022 6.21967C1.73732 6.51256 1.73732 6.98744 2.03022 7.28033L4.49989 9.75M2.24989 6.75H13.1249C14.7817 6.75 16.1249 8.09315 16.1249 9.75V10.875C16.1249 12.5319 14.7817 13.875 13.1249 13.875H8.99989"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -42,6 +42,13 @@ export * from './StorageIcon';
|
|||||||
export * from './LinkIcon';
|
export * from './LinkIcon';
|
||||||
export * from './LinkChainIcon';
|
export * from './LinkChainIcon';
|
||||||
export * from './CursorBoxIcon';
|
export * from './CursorBoxIcon';
|
||||||
|
export * from './CommitIcon';
|
||||||
|
export * from './RocketIcon';
|
||||||
|
export * from './RefreshIcon';
|
||||||
|
export * from './UndoIcon';
|
||||||
|
export * from './LoaderIcon';
|
||||||
|
export * from './MinusCircleIcon';
|
||||||
|
export * from './CopyIcon';
|
||||||
|
|
||||||
// Templates
|
// Templates
|
||||||
export * from './templates';
|
export * from './templates';
|
||||||
|
@ -8,6 +8,8 @@ export const inputTheme = tv(
|
|||||||
'items-center',
|
'items-center',
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
'relative',
|
'relative',
|
||||||
|
'gap-2',
|
||||||
|
'w-full',
|
||||||
'placeholder:text-elements-disabled',
|
'placeholder:text-elements-disabled',
|
||||||
'disabled:cursor-not-allowed',
|
'disabled:cursor-not-allowed',
|
||||||
'disabled:bg-controls-disabled',
|
'disabled:bg-controls-disabled',
|
||||||
@ -27,7 +29,7 @@ export const inputTheme = tv(
|
|||||||
'disabled:shadow-none',
|
'disabled:shadow-none',
|
||||||
'disabled:border-none',
|
'disabled:border-none',
|
||||||
],
|
],
|
||||||
icon: ['text-elements-mid-em'],
|
icon: ['text-elements-low-em'],
|
||||||
iconContainer: [
|
iconContainer: [
|
||||||
'absolute',
|
'absolute',
|
||||||
'inset-y-0',
|
'inset-y-0',
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
import { cn } from 'utils/classnames';
|
||||||
|
import { Tooltip, TooltipProps } from 'components/shared/Tooltip';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import React, {
|
||||||
|
ComponentPropsWithRef,
|
||||||
|
PropsWithChildren,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { PolymorphicProps } from 'types/common';
|
||||||
|
|
||||||
|
interface OverflownTextProps extends ComponentPropsWithRef<'span'> {
|
||||||
|
tooltipProps?: TooltipProps;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ElementType = 'span' | 'div';
|
||||||
|
|
||||||
|
// This component is used to truncate text and show a tooltip if the text is overflown.
|
||||||
|
export const OverflownText = <Element extends ElementType>({
|
||||||
|
tooltipProps,
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
className,
|
||||||
|
as,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<PolymorphicProps<Element, OverflownTextProps>>) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const [isOverflown, setIsOverflown] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current as HTMLElement | null;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
setIsOverflown(element.scrollWidth > element.clientWidth);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const isOverflown = element.scrollWidth > element.clientWidth;
|
||||||
|
setIsOverflown(isOverflown);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', debounce(handleResize, 500));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const Component = as || 'span';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={content ?? children}
|
||||||
|
delayDuration={500}
|
||||||
|
contentProps={{
|
||||||
|
className: 'text-xs',
|
||||||
|
}}
|
||||||
|
open={isOverflown ? undefined : false}
|
||||||
|
{...tooltipProps}
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
ref={ref}
|
||||||
|
className={cn('truncate block', className)} // line-clamp-1 won't work here
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './OverflownText';
|
@ -85,7 +85,7 @@ export const selectTheme = tv({
|
|||||||
size: {
|
size: {
|
||||||
md: {
|
md: {
|
||||||
container: ['min-h-11'],
|
container: ['min-h-11'],
|
||||||
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4', 'py-1'],
|
inputWrapper: ['min-h-11', 'text-sm', 'pl-4', 'pr-4'],
|
||||||
icon: ['h-[18px]', 'w-[18px]'],
|
icon: ['h-[18px]', 'w-[18px]'],
|
||||||
helperText: 'text-sm',
|
helperText: 'text-sm',
|
||||||
helperIcon: ['h-5', 'w-5'],
|
helperIcon: ['h-5', 'w-5'],
|
||||||
@ -93,7 +93,7 @@ export const selectTheme = tv({
|
|||||||
},
|
},
|
||||||
sm: {
|
sm: {
|
||||||
container: ['min-h-8'],
|
container: ['min-h-8'],
|
||||||
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3', 'py-0.5'],
|
inputWrapper: ['min-h-8', 'text-xs', 'pl-3', 'pr-3'],
|
||||||
icon: ['h-4', 'w-4'],
|
icon: ['h-4', 'w-4'],
|
||||||
helperText: 'text-xs',
|
helperText: 'text-xs',
|
||||||
helperIcon: ['h-4', 'w-4'],
|
helperIcon: ['h-4', 'w-4'],
|
||||||
|
@ -3,7 +3,6 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -135,7 +134,9 @@ export const Select = ({
|
|||||||
const theme = selectTheme({ size, error, variant, orientation });
|
const theme = selectTheme({ size, error, variant, orientation });
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null);
|
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(
|
||||||
|
(value as SelectOption) || null,
|
||||||
|
);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||||||
'bottom',
|
'bottom',
|
||||||
@ -166,22 +167,6 @@ export const Select = ({
|
|||||||
}
|
}
|
||||||
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If multiple selection is enabled, ensure the internal state is an array
|
|
||||||
if (multiple) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
// Directly use the provided array
|
|
||||||
setSelectedItems(value);
|
|
||||||
} else {
|
|
||||||
// Reset or set to empty array if the value is not an array
|
|
||||||
setSelectedItems([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For single selection, directly set the selected item
|
|
||||||
setSelectedItem(value as SelectOption);
|
|
||||||
}
|
|
||||||
}, [value, multiple]);
|
|
||||||
|
|
||||||
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
||||||
setSelectedItem(selectedItem);
|
setSelectedItem(selectedItem);
|
||||||
setInputValue(selectedItem ? selectedItem.label : '');
|
setInputValue(selectedItem ? selectedItem.label : '');
|
||||||
@ -194,9 +179,9 @@ export const Select = ({
|
|||||||
addSelectedItem,
|
addSelectedItem,
|
||||||
removeSelectedItem,
|
removeSelectedItem,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
setSelectedItems,
|
|
||||||
reset,
|
reset,
|
||||||
} = useMultipleSelection<SelectOption>({
|
} = useMultipleSelection<SelectOption>({
|
||||||
|
selectedItems: multiple ? (value as SelectOption[]) : [],
|
||||||
onSelectedItemsChange: multiple
|
onSelectedItemsChange: multiple
|
||||||
? undefined
|
? undefined
|
||||||
: ({ selectedItems }) => {
|
: ({ selectedItems }) => {
|
||||||
@ -234,6 +219,7 @@ export const Select = ({
|
|||||||
openMenu,
|
openMenu,
|
||||||
} = useCombobox({
|
} = useCombobox({
|
||||||
items: filteredItems,
|
items: filteredItems,
|
||||||
|
selectedItem: multiple ? null : (value as SelectOption) || null,
|
||||||
// @ts-expect-error – there are two params but we don't need the second one
|
// @ts-expect-error – there are two params but we don't need the second one
|
||||||
isItemDisabled: (item) => item.disabled,
|
isItemDisabled: (item) => item.disabled,
|
||||||
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
||||||
@ -265,16 +251,12 @@ export const Select = ({
|
|||||||
setInputValue('');
|
setInputValue('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedItem: multiple ? null : selectedItem,
|
|
||||||
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
|
// TODO: Make the input value empty when the dropdown is open, has a value, it is not multiple, and searchable
|
||||||
itemToString: (item) => (item && !multiple ? item.label : ''),
|
itemToString: (item) => (item && !multiple ? item.label : ''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = (item: SelectOption) =>
|
||||||
(item: SelectOption) =>
|
multiple ? selectedItems.includes(item) : selectedItem === item;
|
||||||
multiple ? selectedItems.includes(item) : selectedItem === item,
|
|
||||||
[selectedItems, selectedItem, multiple],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
|
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -336,6 +318,7 @@ export const Select = ({
|
|||||||
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
||||||
const isMultipleHasValueButNotSearchable =
|
const isMultipleHasValueButNotSearchable =
|
||||||
multiple && !searchable && selectedItems.length > 0;
|
multiple && !searchable && selectedItems.length > 0;
|
||||||
|
|
||||||
const displayPlaceholder = useMemo(() => {
|
const displayPlaceholder = useMemo(() => {
|
||||||
if (hideValues && isMultipleHasValue) {
|
if (hideValues && isMultipleHasValue) {
|
||||||
return `${selectedItems.length} selected`;
|
return `${selectedItems.length} selected`;
|
||||||
@ -391,6 +374,8 @@ export const Select = ({
|
|||||||
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
|
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
|
||||||
// Add margin to the X icon
|
// Add margin to the X icon
|
||||||
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
||||||
|
// Add padding if there's a left icon
|
||||||
|
'pl-7': leftIcon,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const stepTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: ['relative', 'px-1.5', 'py-1.5', 'flex', 'gap-2', 'items-center'],
|
||||||
|
step: [
|
||||||
|
'bg-base-bg-emphasized',
|
||||||
|
'rounded-full',
|
||||||
|
'w-7',
|
||||||
|
'h-7',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
'text-elements-mid-em',
|
||||||
|
'shadow-button',
|
||||||
|
'shrink-0',
|
||||||
|
],
|
||||||
|
label: [
|
||||||
|
'text-sm',
|
||||||
|
'font-sans',
|
||||||
|
'text-elements-mid-em',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
],
|
||||||
|
connector: [],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: {
|
||||||
|
connector: ['bg-border-interactive-hovered', 'w-px', 'h-3', 'ml-5'],
|
||||||
|
},
|
||||||
|
horizontal: {
|
||||||
|
connector: ['text-border-interactive-hovered', 'h-3', 'w-3'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
true: {
|
||||||
|
step: ['bg-controls-secondary-hovered', 'text-elements-on-secondary'],
|
||||||
|
label: ['text-elements-high-em'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
true: {
|
||||||
|
step: ['text-controls-primary'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: 'vertical',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StepTheme = VariantProps<typeof stepTheme>;
|
67
packages/frontend/src/components/shared/Steps/Step/Step.tsx
Normal file
67
packages/frontend/src/components/shared/Steps/Step/Step.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useCallback, ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { stepTheme, StepTheme } from './Step.theme';
|
||||||
|
import {
|
||||||
|
CheckRoundFilledIcon,
|
||||||
|
ChevronRight,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
|
export interface StepProps extends ComponentPropsWithoutRef<'li'>, StepTheme {
|
||||||
|
/**
|
||||||
|
* The label for the step
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* The index of the step
|
||||||
|
*/
|
||||||
|
index: number;
|
||||||
|
/**
|
||||||
|
* The total number of steps
|
||||||
|
*/
|
||||||
|
currentIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Step = ({
|
||||||
|
label,
|
||||||
|
index,
|
||||||
|
currentIndex,
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: StepProps) => {
|
||||||
|
const theme = stepTheme();
|
||||||
|
|
||||||
|
const active = currentIndex === index;
|
||||||
|
const completed = currentIndex > index;
|
||||||
|
|
||||||
|
const renderConnector = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (index === 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div aria-hidden className={theme.connector({ orientation })}>
|
||||||
|
{orientation === 'horizontal' && <ChevronRight size={12} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[orientation, theme],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderConnector(index)}
|
||||||
|
<li className={theme.wrapper()} {...props}>
|
||||||
|
{
|
||||||
|
<div className={theme.step({ active, completed })}>
|
||||||
|
{completed ? (
|
||||||
|
<CheckRoundFilledIcon className="w-full h-full" />
|
||||||
|
) : (
|
||||||
|
index
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<p className={theme.label()}>{label}</p>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Step';
|
||||||
|
export * from './Step.theme';
|
18
packages/frontend/src/components/shared/Steps/Steps.theme.ts
Normal file
18
packages/frontend/src/components/shared/Steps/Steps.theme.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const stepsTheme = tv({
|
||||||
|
slots: {
|
||||||
|
root: [],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: { root: ['flex', 'flex-col'] },
|
||||||
|
horizontal: { root: ['flex', 'items-center', 'gap-1'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: 'vertical',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StepsTheme = VariantProps<typeof stepsTheme>;
|
42
packages/frontend/src/components/shared/Steps/Steps.tsx
Normal file
42
packages/frontend/src/components/shared/Steps/Steps.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { Fragment, ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { stepsTheme, StepsTheme } from './Steps.theme';
|
||||||
|
import { Step, StepProps, StepTheme } from './Step';
|
||||||
|
|
||||||
|
interface StepsProps
|
||||||
|
extends ComponentPropsWithoutRef<'ul'>,
|
||||||
|
StepsTheme,
|
||||||
|
Pick<StepTheme, 'orientation'> {
|
||||||
|
/**
|
||||||
|
* The index of the current step
|
||||||
|
*/
|
||||||
|
currentIndex: number;
|
||||||
|
/**
|
||||||
|
* The steps to render
|
||||||
|
*/
|
||||||
|
steps: Pick<StepProps, 'label'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Steps = ({
|
||||||
|
currentIndex,
|
||||||
|
steps = [],
|
||||||
|
className,
|
||||||
|
orientation,
|
||||||
|
...props
|
||||||
|
}: StepsProps) => {
|
||||||
|
const theme = stepsTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={theme.root({ class: className, orientation })} {...props}>
|
||||||
|
{steps.map((step, i) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<Step
|
||||||
|
{...step}
|
||||||
|
orientation={orientation}
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
index={i + 1}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
2
packages/frontend/src/components/shared/Steps/index.ts
Normal file
2
packages/frontend/src/components/shared/Steps/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Steps';
|
||||||
|
export * from './Steps.theme';
|
@ -71,6 +71,7 @@ export const tabsTheme = tv({
|
|||||||
'gap-5',
|
'gap-5',
|
||||||
'border-b',
|
'border-b',
|
||||||
'border-transparent',
|
'border-transparent',
|
||||||
|
'overflow-scroll',
|
||||||
// Horizontal – default
|
// Horizontal – default
|
||||||
'data-[orientation=horizontal]:border-border-interactive/10',
|
'data-[orientation=horizontal]:border-border-interactive/10',
|
||||||
// Vertical
|
// Vertical
|
||||||
|
@ -4,7 +4,7 @@ import type { VariantProps } from 'tailwind-variants';
|
|||||||
export const tagTheme = tv(
|
export const tagTheme = tv(
|
||||||
{
|
{
|
||||||
slots: {
|
slots: {
|
||||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||||
icon: [],
|
icon: [],
|
||||||
label: ['font-inter', 'text-xs'],
|
label: ['font-inter', 'text-xs'],
|
||||||
},
|
},
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
import { renderInputs } from './renders/input';
|
import { renderInputs } from './renders/input';
|
||||||
import { RADIO_OPTIONS } from './renders/radio';
|
import { RADIO_OPTIONS } from './renders/radio';
|
||||||
import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls';
|
import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls';
|
||||||
|
import { renderHorizontalSteps, renderVerticalSteps } from './renders/steps';
|
||||||
import {
|
import {
|
||||||
renderTabWithBadges,
|
renderTabWithBadges,
|
||||||
renderTabs,
|
renderTabs,
|
||||||
@ -56,6 +57,19 @@ const Page: React.FC = () => {
|
|||||||
|
|
||||||
<div className="w-full h border border-gray-200 px-20 my-10" />
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Steps</h1>
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-center">
|
||||||
|
{renderVerticalSteps()}
|
||||||
|
{renderHorizontalSteps()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
{/* Tag */}
|
{/* Tag */}
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
41
packages/frontend/src/pages/components/renders/steps.tsx
Normal file
41
packages/frontend/src/pages/components/renders/steps.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Steps } from 'components/shared/Steps';
|
||||||
|
|
||||||
|
export const renderVerticalSteps = () => {
|
||||||
|
return (
|
||||||
|
<Steps
|
||||||
|
currentIndex={1}
|
||||||
|
steps={[
|
||||||
|
{
|
||||||
|
label: 'Create repository',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deploy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `What's next?`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderHorizontalSteps = () => {
|
||||||
|
return (
|
||||||
|
<Steps
|
||||||
|
orientation="horizontal"
|
||||||
|
currentIndex={1}
|
||||||
|
steps={[
|
||||||
|
{
|
||||||
|
label: 'Create repository',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deploy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `What's next?`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -7,6 +7,7 @@ import {
|
|||||||
useParams,
|
useParams,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Project as ProjectType } from 'gql-client';
|
import { Project as ProjectType } from 'gql-client';
|
||||||
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import { useOctokit } from '../../../context/OctokitContext';
|
import { useOctokit } from '../../../context/OctokitContext';
|
||||||
@ -23,6 +24,9 @@ const Id = () => {
|
|||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isDesktopView = useMediaQuery('(min-width: 768px)'); // md:
|
||||||
|
const buttonSize = isDesktopView ? {} : { size: 'sm' as const };
|
||||||
|
|
||||||
const [project, setProject] = useState<ProjectType | null>(null);
|
const [project, setProject] = useState<ProjectType | null>(null);
|
||||||
const [repoUrl, setRepoUrl] = useState('');
|
const [repoUrl, setRepoUrl] = useState('');
|
||||||
|
|
||||||
@ -65,25 +69,32 @@ const Id = () => {
|
|||||||
{project ? (
|
{project ? (
|
||||||
<>
|
<>
|
||||||
<div className="px-6 py-4 flex justify-between items-center gap-4">
|
<div className="px-6 py-4 flex justify-between items-center gap-4">
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="flex items-center justify-center gap-4 overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
className="rounded-full h-11 w-11 p-0"
|
iconOnly
|
||||||
|
className="rounded-full h-11 w-11 p-0 shrink-0"
|
||||||
aria-label="Go back"
|
aria-label="Go back"
|
||||||
leftIcon={<ChevronLeft />}
|
leftIcon={<ChevronLeft />}
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
/>
|
/>
|
||||||
<Heading className="text-2xl font-medium">
|
<Heading className="text-2xl font-medium truncate">
|
||||||
{project?.name}
|
{project?.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<Link to={repoUrl} target="_blank">
|
<Link to={repoUrl} target="_blank">
|
||||||
<Button className="h-11 transition-colors" variant="tertiary">
|
<Button
|
||||||
|
{...buttonSize}
|
||||||
|
className="h-11 transition-colors"
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
Open repo
|
Open repo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button className="h-11 transition-colors">Go to app</Button>
|
<Button {...buttonSize} className="h-11 transition-colors">
|
||||||
|
Go to app
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder />
|
<WavyBorder />
|
||||||
@ -96,9 +107,6 @@ const Id = () => {
|
|||||||
<Tabs.Trigger value="deployments">
|
<Tabs.Trigger value="deployments">
|
||||||
<Link to="deployments">Deployments</Link>
|
<Link to="deployments">Deployments</Link>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="database">
|
|
||||||
<Link to="database">Database</Link>
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="integrations">
|
<Tabs.Trigger value="integrations">
|
||||||
<Link to="integrations">Integrations</Link>
|
<Link to="integrations">Integrations</Link>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
@ -6,11 +6,11 @@ import {
|
|||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import Stepper from '../../../../components/Stepper';
|
|
||||||
import templates from '../../../../assets/templates';
|
import templates from '../../../../assets/templates';
|
||||||
import { Avatar } from 'components/shared/Avatar';
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import { LinkChainIcon } from 'components/shared/CustomIcon';
|
import { LinkChainIcon } from 'components/shared/CustomIcon';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Steps } from 'components/shared/Steps';
|
||||||
|
|
||||||
// TODO: Set dynamic route for template and load details from DB
|
// TODO: Set dynamic route for template and load details from DB
|
||||||
const CreateWithTemplate = () => {
|
const CreateWithTemplate = () => {
|
||||||
@ -68,7 +68,7 @@ const CreateWithTemplate = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 w-5/6 p-6">
|
<div className="grid grid-cols-3 w-5/6 p-6">
|
||||||
<div>
|
<div>
|
||||||
<Stepper activeStep={activeStep} stepperValues={stepperValues} />
|
<Steps currentIndex={activeStep} steps={stepperValues} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Outlet context={{ template }} />
|
<Outlet context={{ template }} />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import templates from 'assets/templates';
|
import templates from 'assets/templates';
|
||||||
import RepositoryList from 'components/projects/create/RepositoryList';
|
import { RepositoryList } from 'components/projects/create/RepositoryList';
|
||||||
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
import ConnectAccount from 'components/projects/create/ConnectAccount';
|
||||||
import { useOctokit } from 'context/OctokitContext';
|
import { useOctokit } from 'context/OctokitContext';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
@ -13,8 +13,8 @@ const NewProject = () => {
|
|||||||
return isAuth ? (
|
return isAuth ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Heading as="h3" className="font-medium text-lg">
|
<Heading as="h3" className="font-medium text-lg pl-1">
|
||||||
Start with template
|
Start with a template
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||||
{templates.map((template) => {
|
{templates.map((template) => {
|
||||||
@ -28,7 +28,7 @@ const NewProject = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Heading as="h3" className="font-medium text-lg mt-10">
|
<Heading as="h3" className="font-medium text-lg mt-10 pl-1 mb-3">
|
||||||
Import a repository
|
Import a repository
|
||||||
</Heading>
|
</Heading>
|
||||||
<RepositoryList octokit={octokit} />
|
<RepositoryList octokit={octokit} />
|
||||||
|
@ -6,6 +6,7 @@ import { WavyBorder } from 'components/shared/WavyBorder';
|
|||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { CrossIcon } from 'components/shared/CustomIcon';
|
import { CrossIcon } from 'components/shared/CustomIcon';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
export interface CreateProjectLayoutProps
|
export interface CreateProjectLayoutProps
|
||||||
extends ComponentPropsWithoutRef<'section'> {}
|
extends ComponentPropsWithoutRef<'section'> {}
|
||||||
@ -16,24 +17,77 @@ export const CreateProjectLayout = ({
|
|||||||
}: CreateProjectLayoutProps) => {
|
}: CreateProjectLayoutProps) => {
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
|
|
||||||
return (
|
const closeBtnLink = `/${orgSlug}`;
|
||||||
<section {...props} className={cn('h-full flex flex-col', className)}>
|
|
||||||
<div className="sticky top-0">
|
const heading = (
|
||||||
<div className="flex px-6 py-4 bg-base-bg items-center gap-4">
|
<Heading as="h2" className="flex-1 text-xl md:text-2xl font-medium">
|
||||||
<Heading as="h2" className="flex-1 text-[24px] font-medium">
|
|
||||||
Create new project
|
Create new project
|
||||||
</Heading>
|
</Heading>
|
||||||
<Link to={`/${orgSlug}`}>
|
);
|
||||||
<Button iconOnly variant="tertiary">
|
|
||||||
<CrossIcon size={18} />
|
return (
|
||||||
</Button>
|
<>
|
||||||
|
{/* Desktop */}
|
||||||
|
<section
|
||||||
|
{...props}
|
||||||
|
className={cn('h-full flex-col hidden md:flex', className)}
|
||||||
|
>
|
||||||
|
<div className="sticky top-0">
|
||||||
|
<div className="flex px-6 py-4 bg-base-bg items-center gap-4">
|
||||||
|
{heading}
|
||||||
|
|
||||||
|
{/* Cannot save btn as variable since responsive variant don't work with compoundVariant */}
|
||||||
|
<Link to={closeBtnLink}>
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
variant="tertiary"
|
||||||
|
leftIcon={<CrossIcon />}
|
||||||
|
aria-label="close"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder />
|
<WavyBorder />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="px-6 h-full flex-1 py-6 overflow-y-auto">
|
<section className="px-6 h-full flex-1 py-6 overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Mobile */}
|
||||||
|
{/* Setting modal={false} so even if modal is active on desktop, it doesn't block clicks */}
|
||||||
|
<Dialog.Root modal={false} open={true}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
{/* Not using <Dialog.Overlay> since modal={false} disables it and its content will not show */}
|
||||||
|
<div className="bg-base-canvas fixed inset-0 md:hidden overflow-y-auto p-1">
|
||||||
|
<Dialog.Content className="min-h-full overflow-hidden rounded-2xl bg-base-bg shadow-card focus:outline-none">
|
||||||
|
{/* Heading */}
|
||||||
|
<div className="flex px-6 py-4 h-20 items-center gap-4">
|
||||||
|
{heading}
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<Link to={closeBtnLink}>
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
variant="tertiary"
|
||||||
|
leftIcon={<CrossIcon />}
|
||||||
|
aria-label="close"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Dialog.Close>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Border */}
|
||||||
|
<WavyBorder />
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
|
<div className="px-4 py-6">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -88,17 +88,17 @@ const DeploymentsTabPanel = () => {
|
|||||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onUpdateDeploymenToProd = async () => {
|
const onUpdateDeploymentToProd = async () => {
|
||||||
await fetchDeployments();
|
await fetchDeployments();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="max-w-[1440px]">
|
||||||
<FilterForm
|
<FilterForm
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
onChange={(value) => setFilterValue(value)}
|
onChange={(value) => setFilterValue(value)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-3">
|
||||||
{Boolean(filteredDeployments.length) ? (
|
{Boolean(filteredDeployments.length) ? (
|
||||||
filteredDeployments.map((deployment, key) => {
|
filteredDeployments.map((deployment, key) => {
|
||||||
return (
|
return (
|
||||||
@ -106,7 +106,7 @@ const DeploymentsTabPanel = () => {
|
|||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
key={key}
|
key={key}
|
||||||
currentDeployment={currentDeployment!}
|
currentDeployment={currentDeployment!}
|
||||||
onUpdate={onUpdateDeploymenToProd}
|
onUpdate={onUpdateDeploymentToProd}
|
||||||
project={project}
|
project={project}
|
||||||
prodBranchDomains={prodBranchDomains}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
|
@ -115,8 +115,8 @@ const OverviewTabPanel = () => {
|
|||||||
}, [project]);
|
}, [project]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-5 gap-[72px]">
|
<div className="grid grid-cols-5 gap-6 md:gap-[72px]">
|
||||||
<div className="col-span-3">
|
<div className="col-span-5 md:col-span-3">
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<Avatar
|
<Avatar
|
||||||
size={48}
|
size={48}
|
||||||
@ -124,13 +124,13 @@ const OverviewTabPanel = () => {
|
|||||||
imageSrc={project.icon}
|
imageSrc={project.icon}
|
||||||
type="blue"
|
type="blue"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 space-y-1">
|
<div className="flex-1 space-y-1 overflow-hidden">
|
||||||
<Heading className="text-lg leading-6 font-medium">
|
<Heading className="text-lg leading-6 font-medium truncate">
|
||||||
{project.name}
|
{project.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
<span className="text-sm text-elements-low-em tracking-tight">
|
<p className="text-sm text-elements-low-em tracking-tight truncate">
|
||||||
{project.subDomain}
|
{project.subDomain}
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||||
|
@ -12,6 +12,9 @@ export default withMT({
|
|||||||
zIndex: {
|
zIndex: {
|
||||||
tooltip: '52',
|
tooltip: '52',
|
||||||
},
|
},
|
||||||
|
letterSpacing: {
|
||||||
|
tight: '-0.084px',
|
||||||
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ['Inter', 'sans-serif'],
|
||||||
display: ['Inter Display', 'sans-serif'],
|
display: ['Inter Display', 'sans-serif'],
|
||||||
@ -84,6 +87,7 @@ export default withMT({
|
|||||||
900: '#0a3a5c',
|
900: '#0a3a5c',
|
||||||
},
|
},
|
||||||
base: {
|
base: {
|
||||||
|
canvas: '#ECF6FE',
|
||||||
bg: '#ffffff',
|
bg: '#ffffff',
|
||||||
'bg-alternate': '#f8fafc',
|
'bg-alternate': '#f8fafc',
|
||||||
'bg-emphasized': '#f1f5f9',
|
'bg-emphasized': '#f1f5f9',
|
||||||
|
23
yarn.lock
23
yarn.lock
@ -3786,6 +3786,27 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-dialog@^1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300"
|
||||||
|
integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||||
|
"@radix-ui/react-focus-guards" "1.0.1"
|
||||||
|
"@radix-ui/react-focus-scope" "1.0.4"
|
||||||
|
"@radix-ui/react-id" "1.0.1"
|
||||||
|
"@radix-ui/react-portal" "1.0.4"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
aria-hidden "^1.1.1"
|
||||||
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
"@radix-ui/react-direction@1.0.1":
|
"@radix-ui/react-direction@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b"
|
||||||
@ -18188,7 +18209,7 @@ use-sync-external-store@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
usehooks-ts@^2.10.0:
|
usehooks-ts@^2.15.1:
|
||||||
version "2.15.1"
|
version "2.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.15.1.tgz#ede348c6f01b4b4fe981e240551624885a2fed83"
|
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.15.1.tgz#ede348c6f01b4b4fe981e240551624885a2fed83"
|
||||||
integrity sha512-AK29ODCt4FT9XleILNbkbjjmkRCNaQrgxQEkvqHjlnT76iPXzTFGvK2Y/s83JEdSxRp43YEnSa3bYBEV6HZ26Q==
|
integrity sha512-AK29ODCt4FT9XleILNbkbjjmkRCNaQrgxQEkvqHjlnT76iPXzTFGvK2Y/s83JEdSxRp43YEnSa3bYBEV6HZ26Q==
|
||||||
|
Loading…
Reference in New Issue
Block a user