Merge branch 'main' into andrehadianto/T-4904-home-org-switcher
This commit is contained in:
commit
410375f8c7
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
||||
{
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
"tv\\('([^)]*)\\')",
|
||||
"(?:'|\"|`)([^\"'`]*)(?:'|\"|`)"
|
||||
]
|
||||
}
|
||||
|
@ -9,14 +9,6 @@ if [[ -d "$DEST_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$PKG_DIR/.env" ]]; then
|
||||
echo "Using existing .env file"
|
||||
else
|
||||
mv "$PKG_DIR/.env.example" "$PKG_DIR/.env"
|
||||
echo "Created .env file. Please populate with the correct values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
|
@ -12,12 +12,14 @@ import {
|
||||
ApplicationDeploymentRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||
import { sleep } from './utils';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
const SLEEP_DURATION = 1000;
|
||||
|
||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||
export class Registry {
|
||||
@ -111,16 +113,21 @@ export class Registry {
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await this.registry.setName(
|
||||
{
|
||||
cid: result.data.id,
|
||||
@ -139,9 +146,9 @@ export class Registry {
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
packageJsonName: string,
|
||||
repository: string,
|
||||
environmentVariables: { [key: string]: string }
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
@ -160,7 +167,7 @@ export class Registry {
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${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
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
@ -178,6 +185,7 @@ export class Registry {
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
@ -211,11 +219,12 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationRecord ids
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(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 (
|
||||
userId: string,
|
||||
octokit: Octokit,
|
||||
data: DeepPartial<Deployment>,
|
||||
recordData: { repoUrl?: string } = {}
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(
|
||||
@ -407,13 +406,10 @@ export class Service {
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
if (!recordData.repoUrl) {
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||
const repoUrl = (await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo
|
||||
});
|
||||
recordData.repoUrl = repoDetails.html_url;
|
||||
}
|
||||
})).data.html_url;
|
||||
|
||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
@ -422,7 +418,7 @@ export class Service {
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl: recordData.repoUrl
|
||||
repoUrl
|
||||
});
|
||||
|
||||
// Update previous deployment with prod branch domain
|
||||
@ -464,11 +460,23 @@ export class Service {
|
||||
{
|
||||
deployment: newDeployment,
|
||||
appName: repo,
|
||||
packageJsonName: packageJSON.name,
|
||||
repository: recordData.repoUrl,
|
||||
environmentVariables: environmentVariablesObj
|
||||
repository: repoUrl,
|
||||
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 });
|
||||
|
||||
return newDeployment;
|
||||
@ -498,8 +506,6 @@ export class Service {
|
||||
per_page: 1
|
||||
});
|
||||
|
||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||
|
||||
// Create deployment with prod branch and latest commit
|
||||
await this.createDeployment(user.id,
|
||||
octokit,
|
||||
@ -510,9 +516,6 @@ export class Service {
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message
|
||||
},
|
||||
{
|
||||
repoUrl: repoDetails.html_url
|
||||
}
|
||||
);
|
||||
|
||||
@ -555,8 +558,14 @@ export class Service {
|
||||
}
|
||||
|
||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||
const { repository, ref, head_commit: headCommit } = data;
|
||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||
const { repository, ref, head_commit: headCommit, deleted } = data;
|
||||
|
||||
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({
|
||||
where: { repository: repository.full_name }
|
||||
});
|
||||
|
@ -24,6 +24,7 @@ export interface GitPushEventPayload {
|
||||
id: string;
|
||||
message: string;
|
||||
};
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export interface AppDeploymentRecordAttributes {
|
||||
|
@ -66,3 +66,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
```
|
||||
./deploy-frontend.sh
|
||||
|
@ -2,8 +2,8 @@ services:
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
chainId: laconic_9000-1
|
||||
gas: 1000000
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||
|
@ -1,11 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
REGISTRY_BOND_ID="8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(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}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat > ./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat > ./records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: snowballtools-base-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully."
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
CONFIG_FILE=config.yml
|
||||
RCD_APP_VERSION="0.1.3"
|
||||
REPO_REF="513ca69d01bee857cf207a0605483205b384e218"
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
@ -15,8 +71,11 @@ echo $RECORD_ID
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${RCD_APP_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${REPO_REF}" "$RECORD_ID"
|
||||
sleep 2
|
||||
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"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
@ -30,6 +89,7 @@ fi
|
||||
|
||||
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')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
@ -1,21 +1,18 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@0.1.3
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.3
|
||||
name: snowballtools-base-frontend@0.1.7
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.7
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
# If GitHub client ID is changed, same ID and corresponding secret has to be set in backend config
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
# New config env after changes for image upload PWA
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
# Set CURRENT_DATE_TIME; Use command date -u
|
||||
note: Added by Snowball @ Tue Feb 27 17:24:06 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_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||
repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340
|
||||
|
@ -1,10 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.11
|
||||
repository_ref: 513ca69d01bee857cf207a0605483205b384e218
|
||||
version: 0.0.1
|
||||
repository_ref: 1ff5ab3dfdba9dcf5dd1cb0f9435bd863a6d0340
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
# name is set to repo name
|
||||
name: snowballtools-base-frontend
|
||||
# app_version is set from package.json
|
||||
app_version: 0.1.3
|
||||
app_version: 0.1.7
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
@ -8,6 +8,7 @@
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^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-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
@ -48,7 +49,7 @@
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
|
@ -18,14 +18,14 @@ export default [
|
||||
name: 'Kotlin',
|
||||
icon: 'kotlin',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'React Native',
|
||||
icon: 'react-native',
|
||||
repoFullName: '',
|
||||
isComingSoon: false,
|
||||
isComingSoon: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
|
@ -1,13 +1,22 @@
|
||||
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)
|
||||
.shiftTo('days', 'hours', 'minutes', 'seconds')
|
||||
.toObject();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
{...props}
|
||||
className={cn('text-sm text-elements-mid-em', props?.className)}
|
||||
>
|
||||
{formatTime.days !== 0 && <span>{formatTime.days}d </span>}
|
||||
{formatTime.hours !== 0 && <span>{formatTime.hours}h </span>}
|
||||
{formatTime.minutes !== 0 && <span>{formatTime.minutes}m </span>}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond from './FormatMilliSecond';
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
|
||||
const setStopWatchOffset = (time: string) => {
|
||||
const providedTime = new Date(time);
|
||||
@ -11,13 +11,17 @@ const setStopWatchOffset = (time: string) => {
|
||||
return currentTime;
|
||||
};
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp }: { offsetTimestamp: Date }) => {
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} />;
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
export { Stopwatch, setStopWatchOffset };
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
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 { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
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 Deploy = () => {
|
||||
@ -31,27 +34,27 @@ const Deploy = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div>
|
||||
<h4>Deployment started ...</h4>
|
||||
<div className="flex">
|
||||
^
|
||||
<div className="space-y-7">
|
||||
<div className="flex justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<Heading as="h4" className="md:text-lg font-medium">
|
||||
Deployment started ...
|
||||
</Heading>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleOpen}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
variant="tertiary"
|
||||
leftIcon={<WarningIcon size={16} />}
|
||||
>
|
||||
^ Cancel
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
dialogTitle="Cancel deployment?"
|
||||
handleOpen={handleOpen}
|
||||
@ -66,6 +69,8 @@ const Deploy = () => {
|
||||
</Typography>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
@ -89,6 +94,7 @@ const Deploy = () => {
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,11 +1,22 @@
|
||||
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 FormatMillisecond from '../../FormatMilliSecond';
|
||||
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 {
|
||||
PROCESSING = 'progress',
|
||||
@ -28,61 +39,115 @@ const DeployStep = ({
|
||||
startTime,
|
||||
processTime,
|
||||
}: 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 (
|
||||
<div className="border-b-2">
|
||||
<div className="flex justify-between p-2 gap-2">
|
||||
{status === DeployStatus.NOT_STARTED && <div>{step}</div>}
|
||||
{status === DeployStatus.PROCESSING && <div>O</div>}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<div>
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<button
|
||||
className={cn(
|
||||
'flex justify-between w-full py-5 gap-2',
|
||||
disableCollapse && 'cursor-auto',
|
||||
)}
|
||||
tabIndex={disableCollapse ? -1 : undefined}
|
||||
onClick={() => {
|
||||
setCollapse(!collapse);
|
||||
if (!disableCollapse) {
|
||||
setIsOpen((val) => !val);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{collapse ? '-' : '+'}
|
||||
</button>
|
||||
<div className={cn('grow flex items-center gap-3')}>
|
||||
{/* 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 className="grow">{title}</div>
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<>
|
||||
^<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</>
|
||||
<LoaderIcon className="animate-spin text-elements-link" />
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
<>
|
||||
^<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</>
|
||||
<div className="text-controls-primary">
|
||||
{!isOpen && <PlusIcon size={24} />}
|
||||
{isOpen && <MinusCircleIcon size={24} />}
|
||||
</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) => {
|
||||
return (
|
||||
<Typography
|
||||
variant="small"
|
||||
color="gray"
|
||||
key={key}
|
||||
placeholder={''}
|
||||
>
|
||||
<p className="font-mono" key={key}>
|
||||
{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
|
||||
size="sm"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||
toast.success('Logs copied');
|
||||
toast({
|
||||
title: 'Logs copied',
|
||||
variant: 'success',
|
||||
id: 'logs',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}}
|
||||
color="blue"
|
||||
placeholder={''}
|
||||
leftIcon={<CopyIcon size={16} />}
|
||||
>
|
||||
^ Copy log
|
||||
Copy log
|
||||
</Button>
|
||||
</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 { 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 './ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||
import AsyncSelect from '../../shared/AsyncSelect';
|
||||
import { GithubIcon } from 'components/shared/CustomIcon';
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
GithubIcon,
|
||||
SearchIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
@ -18,9 +22,9 @@ interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
@ -35,7 +39,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount(user.data.login);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
@ -54,7 +58,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
let query = `${debouncedSearchedRepo} in:name fork:true`;
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
@ -69,7 +73,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAccount === gitUser.login) {
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
const result = await octokit.rest.repos.listForAuthenticatedUser({
|
||||
per_page: REPOS_PER_PAGE,
|
||||
affiliation: 'owner',
|
||||
@ -78,7 +82,9 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
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');
|
||||
|
||||
const result = await octokit.rest.repos.listForOrg({
|
||||
@ -96,7 +102,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const handleResetFilters = useCallback(() => {
|
||||
assert(gitUser, 'Git user is not available');
|
||||
setSearchedRepo(DEFAULT_SEARCHED_REPO);
|
||||
setSelectedAccount(gitUser.login);
|
||||
setSelectedAccount({ label: gitUser.login, value: gitUser.login });
|
||||
}, [gitUser]);
|
||||
|
||||
const accounts = useMemo(() => {
|
||||
@ -107,35 +113,52 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
return [gitUser, ...orgs];
|
||||
}, [octokit, orgs, gitUser]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return accounts.map((account) => ({
|
||||
label: account.login,
|
||||
value: account.login,
|
||||
leftIcon: <GithubIcon />,
|
||||
}));
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex gap-2 mb-2 items-center">
|
||||
<div className="basis-1/3">
|
||||
<AsyncSelect
|
||||
<section className="space-y-3">
|
||||
{/* Dropdown and search */}
|
||||
<div className="flex flex-col lg:flex-row gap-0 lg:gap-3 items-center">
|
||||
<div className="lg:basis-1/3 w-full">
|
||||
<Select
|
||||
options={options}
|
||||
placeholder="Select a repository"
|
||||
value={selectedAccount}
|
||||
onChange={(value) => setSelectedAccount(value!)}
|
||||
>
|
||||
{accounts.map((account) => (
|
||||
<Option key={account.id} value={account.login}>
|
||||
<div className="flex items-center gap-2 justify-start">
|
||||
<GithubIcon /> {account.login}
|
||||
leftIcon={selectedAccount ? <GithubIcon /> : undefined}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
onChange={(value) => setSelectedAccount(value as SelectOption)}
|
||||
/>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
</div>
|
||||
<div className="basis-2/3 flex-grow flex items-center">
|
||||
<SearchBar
|
||||
<div className="basis-2/3 flex w-full flex-grow">
|
||||
<Input
|
||||
className="w-full"
|
||||
value={searchedRepo}
|
||||
onChange={(event) => setSearchedRepo(event.target.value)}
|
||||
placeholder="Search for repository"
|
||||
leftIcon={<SearchIcon />}
|
||||
onChange={(e) => setSearchedRepo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Repository list */}
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
<div className="flex flex-col gap-2">
|
||||
{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="text-center">
|
||||
@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryList;
|
@ -0,0 +1 @@
|
||||
export * from './RepositoryList';
|
@ -1,3 +1,4 @@
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
@ -6,8 +7,7 @@ import {
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import React, { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
@ -24,9 +24,13 @@ export interface TemplateCardProps extends ComponentPropsWithoutRef<'div'> {
|
||||
isGitAuth: boolean;
|
||||
}
|
||||
|
||||
export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||
template,
|
||||
isGitAuth,
|
||||
}: TemplateCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (template?.isComingSoon) {
|
||||
@ -38,7 +42,9 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
});
|
||||
}
|
||||
if (isGitAuth) {
|
||||
return navigate(`/template?templateId=${template.id}`);
|
||||
return navigate(
|
||||
`/${orgSlug}/projects/create/template?templateId=${template.id}`,
|
||||
);
|
||||
}
|
||||
return toast({
|
||||
id: 'connect-git-account',
|
||||
@ -46,12 +52,12 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}, [isGitAuth, navigate, template, toast]);
|
||||
}, [orgSlug, dismiss, isGitAuth, navigate, template, toast]);
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
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,
|
||||
},
|
||||
@ -80,6 +86,6 @@ export const TemplateCard = ({ template, isGitAuth }: TemplateCardProps) => {
|
||||
<ArrowRightCircleIcon />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,31 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import React from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
Domain,
|
||||
Environment,
|
||||
Project,
|
||||
Domain,
|
||||
DeploymentStatus,
|
||||
Deployment,
|
||||
} from 'gql-client';
|
||||
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import {
|
||||
Menu,
|
||||
MenuHandler,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Typography,
|
||||
Chip,
|
||||
ChipProps,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
BranchStrokeIcon,
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CommitIcon,
|
||||
LoadingIcon,
|
||||
WarningIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { DeploymentMenu } from './DeploymentMenu';
|
||||
|
||||
interface DeployDetailsCardProps {
|
||||
deployment: Deployment;
|
||||
@ -35,10 +32,12 @@ interface DeployDetailsCardProps {
|
||||
prodBranchDomains: Domain[];
|
||||
}
|
||||
|
||||
const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
|
||||
[DeploymentStatus.Building]: 'blue',
|
||||
[DeploymentStatus.Ready]: 'green',
|
||||
[DeploymentStatus.Error]: 'red',
|
||||
const STATUS_COLORS: {
|
||||
[key in DeploymentStatus]: TagTheme['type'];
|
||||
} = {
|
||||
[DeploymentStatus.Building]: 'emphasized',
|
||||
[DeploymentStatus.Ready]: 'positive',
|
||||
[DeploymentStatus.Error]: 'negative',
|
||||
};
|
||||
|
||||
const DeploymentDetailsCard = ({
|
||||
@ -48,241 +47,99 @@ const DeploymentDetailsCard = ({
|
||||
project,
|
||||
prodBranchDomains,
|
||||
}: DeployDetailsCardProps) => {
|
||||
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 getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||
if (status === DeploymentStatus.Building) {
|
||||
return <LoadingIcon className="animate-spin" />;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
if (status === DeploymentStatus.Ready) {
|
||||
return <CheckRoundFilledIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
if (status === DeploymentStatus.Error) {
|
||||
return <WarningIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
||||
<div className="col-span-3">
|
||||
<div className="flex">
|
||||
<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="flex-1 max-w-[30%] space-y-2">
|
||||
{/* 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}
|
||||
</Typography>
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
</div>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Chip
|
||||
value={deployment.status}
|
||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
||||
variant="ghost"
|
||||
icon={<i>^</i>}
|
||||
/>
|
||||
|
||||
{/* DEPLOYMENT STATUS */}
|
||||
<div className="w-[10%] max-w-[110px]">
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.branch}
|
||||
</Typography>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||
|
||||
{/* DEPLOYMENT COMMIT DETAILS */}
|
||||
<div className="text-sm w-[25%] space-y-2 text-elements-low-em">
|
||||
<span className="flex gap-1.5 items-center">
|
||||
<BranchStrokeIcon className="h-4 w-4" />
|
||||
{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}
|
||||
</Typography>
|
||||
</OverflownText>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Typography color="gray" className="grow" placeholder={''}>
|
||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
||||
<Tooltip content={deployment.createdBy.name}>
|
||||
|
||||
{/* DEPLOYMENT INFOs */}
|
||||
<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">
|
||||
<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 ?? '')}
|
||||
</Tooltip>
|
||||
</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>
|
||||
</OverflownText>
|
||||
</div>
|
||||
<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
|
||||
<DeploymentMenu
|
||||
className="ml-auto"
|
||||
deployment={deployment}
|
||||
chip={{
|
||||
value: 'New Deployment',
|
||||
color: 'orange',
|
||||
}}
|
||||
currentDeployment={currentDeployment}
|
||||
onUpdate={onUpdate}
|
||||
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>
|
||||
</ConfirmDialog>
|
||||
)}
|
||||
<AssignDomainDialog
|
||||
open={assignDomainDialog}
|
||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
/>
|
||||
</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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,10 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
import { IconButton, Option, Select } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../../SearchBar';
|
||||
import DatePicker from '../../../DatePicker';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import {
|
||||
CheckRadioOutlineIcon,
|
||||
CrossCircleIcon,
|
||||
LoaderIcon,
|
||||
SearchIcon,
|
||||
TrendingIcon,
|
||||
WarningTriangleIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
@ -15,8 +22,8 @@ export enum StatusOptions {
|
||||
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: StatusOptions;
|
||||
updateAtRange?: DateRange;
|
||||
status: StatusOptions | string;
|
||||
updateAtRange?: Value;
|
||||
}
|
||||
|
||||
interface FilterFormProps {
|
||||
@ -27,7 +34,7 @@ interface FilterFormProps {
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
const [dateRange, setDateRange] = useState<Value>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
@ -43,46 +50,68 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
setDateRange(value.updateAtRange);
|
||||
}, [value]);
|
||||
|
||||
const getOptionIcon = (status: StatusOptions) => {
|
||||
switch (status) {
|
||||
case StatusOptions.BUILDING:
|
||||
return <LoaderIcon />;
|
||||
case StatusOptions.READY:
|
||||
return <CheckRadioOutlineIcon />;
|
||||
case StatusOptions.ERROR:
|
||||
return <WarningTriangleIcon />;
|
||||
case StatusOptions.ALL_STATUS:
|
||||
default:
|
||||
return <TrendingIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = Object.values(StatusOptions).map((status) => ({
|
||||
label: status,
|
||||
value: status,
|
||||
leftIcon: getOptionIcon(status),
|
||||
}));
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchedBranch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
|
||||
<div className="col-span-4">
|
||||
<SearchBar
|
||||
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-0">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Input
|
||||
placeholder="Search branches"
|
||||
leftIcon={<SearchIcon />}
|
||||
rightIcon={
|
||||
searchedBranch && <CrossCircleIcon onClick={handleReset} />
|
||||
}
|
||||
value={searchedBranch}
|
||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||
onChange={(e) => setSearchedBranch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
<div className="col-span-2 flex items-center">
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
selectRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
onReset={() => setDateRange(undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 relative">
|
||||
<div className="col-span-2 flex items-center">
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
|
||||
options={statusOptions}
|
||||
clearable
|
||||
placeholder="All status"
|
||||
>
|
||||
{Object.values(StatusOptions).map((status) => (
|
||||
<Option
|
||||
className={status === StatusOptions.ALL_STATUS ? 'hidden' : ''}
|
||||
key={status}
|
||||
value={status}
|
||||
>
|
||||
^ {status}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{selectedStatus !== StatusOptions.ALL_STATUS && (
|
||||
<div className="absolute end-1 inset-y-0 my-auto h-8">
|
||||
<IconButton
|
||||
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
|
||||
className="rounded-full"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
value={
|
||||
selectedStatus
|
||||
? { label: selectedStatus, value: selectedStatus }
|
||||
: undefined
|
||||
}
|
||||
onChange={(item) =>
|
||||
setSelectedStatus((item as SelectOption).value as StatusOptions)
|
||||
}
|
||||
onClear={() => setSelectedStatus('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitCommitWithBranch } from 'types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
export const Activity = ({
|
||||
activities,
|
||||
}: {
|
||||
activities: GitCommitWithBranch[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="col-span-5 md:col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
See all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return <ActivityCard activity={activity} key={`activity-${index}`} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../types';
|
||||
import { GitCommitWithBranch } from '../../../../../types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowRightCircleFilledIcon,
|
||||
BranchIcon,
|
||||
BranchStrokeIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
@ -50,7 +50,7 @@ const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||
<span className="w-0.5 h-0.5 rounded-full bg-border-interactive-hovered"></span>
|
||||
<span className="flex justify-center items-center gap-1.5">
|
||||
<div>
|
||||
<BranchIcon />
|
||||
<BranchStrokeIcon className="w-3 h-3" />
|
||||
</div>
|
||||
<span
|
||||
title={activity.branch.name}
|
@ -0,0 +1 @@
|
||||
export * from './Activity';
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
interface OverviewInfoProps {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OverviewInfo = ({
|
||||
label,
|
||||
icon,
|
||||
children,
|
||||
}: PropsWithChildren<OverviewInfoProps>) => {
|
||||
const styledIcon = cloneElement({
|
||||
element: icon,
|
||||
className: 'w-4 h-4',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 py-3 text-sm items-center">
|
||||
<div className="flex gap-2 items-center text-elements-high-em">
|
||||
{styledIcon}
|
||||
{label}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -2,7 +2,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
||||
|
||||
export const avatarTheme = tv(
|
||||
{
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden'],
|
||||
base: ['relative', 'block', 'rounded-full', 'overflow-hidden', 'shrink-0'],
|
||||
slots: {
|
||||
image: [
|
||||
'h-full',
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
|
||||
import './Calendar.css';
|
||||
import { format } from 'date-fns';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
type ValuePiece = Date | null;
|
||||
export type Value = ValuePiece | [ValuePiece, ValuePiece];
|
||||
@ -63,6 +64,11 @@ export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
|
||||
* @returns None
|
||||
*/
|
||||
onCancel?: () => void;
|
||||
/**
|
||||
* Optional callback function that is called when a reset action is triggered.
|
||||
* @returns None
|
||||
*/
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,6 +86,7 @@ export const Calendar = ({
|
||||
actions,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onReset,
|
||||
onChange: onChangeProp,
|
||||
...props
|
||||
}: CalendarProps): JSX.Element => {
|
||||
@ -217,6 +224,11 @@ export const Calendar = ({
|
||||
[setValue, setActiveDate, changeNavigationLabel, selectRange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setValue(null);
|
||||
onReset?.();
|
||||
}, [setValue, onReset]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
@ -276,12 +288,20 @@ export const Calendar = ({
|
||||
{/* Footer or CTA */}
|
||||
<div
|
||||
{...footerProps}
|
||||
className={footer({ className: footerProps?.className })}
|
||||
className={cn(footer({ className: footerProps?.className }), {
|
||||
'justify-between': Boolean(value),
|
||||
})}
|
||||
>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<>
|
||||
{value && (
|
||||
<Button variant="danger" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<div className="space-x-3">
|
||||
<Button variant="tertiary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
@ -291,6 +311,7 @@ export const Calendar = ({
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const BranchStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.66667 4.99984C5.58714 4.99984 6.33333 4.25365 6.33333 3.33317C6.33333 2.4127 5.58714 1.6665 4.66667 1.6665C3.74619 1.6665 3 2.4127 3 3.33317C3 4.25365 3.74619 4.99984 4.66667 4.99984ZM4.66667 4.99984V10.9998M4.66667 10.9998C3.74619 10.9998 3 11.746 3 12.6665C3 13.587 3.74619 14.3332 4.66667 14.3332C5.58714 14.3332 6.33333 13.587 6.33333 12.6665C6.33333 11.746 5.58714 10.9998 4.66667 10.9998ZM4.66667 10.9998V9.33317C4.66667 8.59679 5.26362 7.99984 6 7.99984H10C10.7364 7.99984 11.3333 7.40288 11.3333 6.6665V4.99984M11.3333 4.99984C12.2538 4.99984 13 4.25365 13 3.33317C13 2.4127 12.2538 1.6665 11.3333 1.6665C10.4129 1.6665 9.66667 2.4127 9.66667 3.33317C9.66667 4.25365 10.4129 4.99984 11.3333 4.99984Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CalendarDaysIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2.3335 5.00016H13.6668M4.33349 13.6668H11.6668C12.7714 13.6668 13.6668 12.7714 13.6668 11.6668V4.3335C13.6668 3.22893 12.7714 2.3335 11.6668 2.3335H4.3335C3.22893 2.3335 2.3335 3.22893 2.3335 4.33349V11.6668C2.3335 12.7714 3.22893 13.6668 4.33349 13.6668Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 7.8335C4.6665 7.55735 4.89036 7.3335 5.1665 7.3335C5.44265 7.3335 5.6665 7.55735 5.6665 7.8335C5.6665 8.10964 5.44265 8.3335 5.1665 8.3335C4.89036 8.3335 4.6665 8.10964 4.6665 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.6665 10.8335C4.6665 10.5574 4.89036 10.3335 5.1665 10.3335C5.44265 10.3335 5.6665 10.5574 5.6665 10.8335C5.6665 11.1096 5.44265 11.3335 5.1665 11.3335C4.89036 11.3335 4.6665 11.1096 4.6665 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 10.8335C7.49984 10.5574 7.72369 10.3335 7.99984 10.3335C8.27598 10.3335 8.49984 10.5574 8.49984 10.8335C8.49984 11.1096 8.27598 11.3335 7.99984 11.3335C7.72369 11.3335 7.49984 11.1096 7.49984 10.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.49984 7.8335C7.49984 7.55735 7.72369 7.3335 7.99984 7.3335C8.27598 7.3335 8.49984 7.55735 8.49984 7.8335C8.49984 8.10964 8.27598 8.3335 7.99984 8.3335C7.72369 8.3335 7.49984 8.10964 7.49984 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M10.3332 7.8335C10.3332 7.55735 10.557 7.3335 10.8332 7.3335C11.1093 7.3335 11.3332 7.55735 11.3332 7.8335C11.3332 8.10964 11.1093 8.3335 10.8332 8.3335C10.557 8.3335 10.3332 8.10964 10.3332 7.8335Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="19"
|
||||
viewBox="0 0 18 19"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z"
|
||||
fill="currentColor"
|
||||
d="M2.625 7.125H15.375M5.625 3.375V1.875M12.375 3.375V1.875M4.125 15.375H13.875C14.7034 15.375 15.375 14.7034 15.375 13.875V4.875C15.375 4.04657 14.7034 3.375 13.875 3.375H4.125C3.29657 3.375 2.625 4.04657 2.625 4.875V13.875C2.625 14.7034 3.29657 15.375 4.125 15.375Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRadioOutlineIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.25 7.125L7.875 11.25L6.375 9.75M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -3,17 +3,11 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CheckRoundFilledIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<CustomIcon width="20" height="20" viewBox="0 0 20 20" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.774 10.1333C16.1237 9.70582 16.0607 9.0758 15.6332 8.72607C15.2058 8.37635 14.5758 8.43935 14.226 8.86679L10.4258 13.5116L9.20711 12.2929C8.81658 11.9024 8.18342 11.9024 7.79289 12.2929C7.40237 12.6834 7.40237 13.3166 7.79289 13.7071L9.79289 15.7071C9.99267 15.9069 10.2676 16.0129 10.5498 15.9988C10.832 15.9847 11.095 15.8519 11.274 15.6333L15.774 10.1333Z"
|
||||
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"
|
||||
/>
|
||||
</CustomIcon>
|
||||
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CirclePlaceholderOnIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</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,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CrossCircleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.5 9C1.5 4.85786 4.85786 1.5 9 1.5C13.1421 1.5 16.5 4.85786 16.5 9C16.5 13.1421 13.1421 16.5 9 16.5C4.85786 16.5 1.5 13.1421 1.5 9ZM7.01516 6.48484C6.86872 6.33839 6.63128 6.33839 6.48484 6.48484C6.33839 6.63128 6.33839 6.86872 6.48484 7.01516L8.46967 9L6.48484 10.9848C6.33839 11.1313 6.33839 11.3687 6.48484 11.5152C6.63128 11.6616 6.86872 11.6616 7.01516 11.5152L9 9.53033L10.9848 11.5152C11.1313 11.6616 11.3687 11.6616 11.5152 11.5152C11.6616 11.3687 11.6616 11.1313 11.5152 10.9848L9.53033 9L11.5152 7.01516C11.6616 6.86872 11.6616 6.63128 11.5152 6.48484C11.3687 6.33839 11.1313 6.33839 10.9848 6.48484L9 8.46967L7.01516 6.48484Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const CursorBoxIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13.5 7V3.83333C13.5 3.09695 12.903 2.5 12.1667 2.5H3.83333C3.09695 2.5 2.5 3.09695 2.5 3.83333V12.1667C2.5 12.903 3.09695 13.5 3.83333 13.5H7M9.43391 14.0852L7.5218 7.9391C7.44202 7.68269 7.68269 7.44202 7.9391 7.5218L14.0852 9.43391C14.3658 9.52122 14.4043 9.90262 14.1468 10.0443L11.5848 11.4534C11.5294 11.4838 11.4838 11.5294 11.4534 11.5848L10.0443 14.1468C9.90262 14.4043 9.52122 14.3658 9.43391 14.0852Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.05263"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const GithubStrokeIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13.0194 4.73001C13.1722 4.23701 13.2212 3.71768 13.1634 3.20479C13.1055 2.6919 12.942 2.19656 12.6831 1.75001C12.6392 1.67398 12.5761 1.61085 12.5001 1.56697C12.424 1.52308 12.3378 1.49999 12.25 1.50001C11.6676 1.49879 11.093 1.6338 10.5721 1.89425C10.0512 2.1547 9.59845 2.53337 9.25 3.00001H7.75C7.40155 2.53337 6.94878 2.1547 6.42788 1.89425C5.90698 1.6338 5.33238 1.49879 4.75 1.50001C4.66221 1.49999 4.57597 1.52308 4.49994 1.56697C4.4239 1.61085 4.36077 1.67398 4.31687 1.75001C4.05803 2.19656 3.89452 2.6919 3.83664 3.20479C3.77877 3.71768 3.8278 4.23701 3.98062 4.73001C3.6717 5.26921 3.50623 5.87862 3.5 6.50001V7.00001C3.50105 7.84601 3.80817 8.66306 4.36464 9.30029C4.92111 9.93752 5.68935 10.3519 6.5275 10.4669C6.18538 10.9047 5.99968 11.4444 6 12V12.5H4.5C4.10218 12.5 3.72064 12.342 3.43934 12.0607C3.15804 11.7794 3 11.3978 3 11C3 10.6717 2.93534 10.3466 2.8097 10.0433C2.68406 9.73999 2.49991 9.46439 2.26777 9.23224C2.03562 9.00009 1.76002 8.81595 1.45671 8.69031C1.15339 8.56467 0.828305 8.50001 0.5 8.50001C0.367392 8.50001 0.240215 8.55269 0.146447 8.64645C0.0526784 8.74022 0 8.8674 0 9.00001C0 9.13262 0.0526784 9.25979 0.146447 9.35356C0.240215 9.44733 0.367392 9.50001 0.5 9.50001C0.897825 9.50001 1.27936 9.65804 1.56066 9.93935C1.84196 10.2207 2 10.6022 2 11C2 11.663 2.26339 12.2989 2.73223 12.7678C3.20107 13.2366 3.83696 13.5 4.5 13.5H6V14.5C6 14.6326 6.05268 14.7598 6.14645 14.8536C6.24021 14.9473 6.36739 15 6.5 15C6.63261 15 6.75979 14.9473 6.85355 14.8536C6.94732 14.7598 7 14.6326 7 14.5V12C7 11.6022 7.15804 11.2207 7.43934 10.9393C7.72064 10.658 8.10218 10.5 8.5 10.5C8.89782 10.5 9.27936 10.658 9.56066 10.9393C9.84196 11.2207 10 11.6022 10 12V14.5C10 14.6326 10.0527 14.7598 10.1464 14.8536C10.2402 14.9473 10.3674 15 10.5 15C10.6326 15 10.7598 14.9473 10.8536 14.8536C10.9473 14.7598 11 14.6326 11 14.5V12C11.0003 11.4444 10.8146 10.9047 10.4725 10.4669C11.3107 10.3519 12.0789 9.93752 12.6354 9.30029C13.1918 8.66306 13.4989 7.84601 13.5 7.00001V6.50001C13.4938 5.87862 13.3283 5.26921 13.0194 4.73001ZM12.5 7.00001C12.5 7.66305 12.2366 8.29893 11.7678 8.76778C11.2989 9.23662 10.663 9.50001 10 9.50001H7C6.33696 9.50001 5.70107 9.23662 5.23223 8.76778C4.76339 8.29893 4.5 7.66305 4.5 7.00001V6.50001C4.50613 6.00002 4.65582 5.51233 4.93125 5.09501C4.98259 5.02733 5.01585 4.94769 5.02788 4.8636C5.03991 4.77951 5.03031 4.69374 5 4.61438C4.86976 4.27851 4.80709 3.92023 4.81556 3.56009C4.82403 3.19995 4.90349 2.84501 5.04938 2.51563C5.45857 2.55965 5.85262 2.6952 6.2023 2.91224C6.55198 3.12928 6.84832 3.42223 7.06938 3.76938C7.11442 3.83982 7.17641 3.89784 7.24968 3.93813C7.32294 3.97842 7.40514 3.9997 7.48875 4.00001H9.51062C9.59455 4.00001 9.67713 3.97889 9.75075 3.93858C9.82437 3.89828 9.88666 3.84009 9.93188 3.76938C10.1529 3.4222 10.4492 3.12922 10.7989 2.91218C11.1486 2.69514 11.5427 2.55961 11.9519 2.51563C12.0976 2.8451 12.1768 3.20007 12.1851 3.56021C12.1933 3.92036 12.1304 4.27859 12 4.61438C11.9698 4.69298 11.9597 4.77788 11.9706 4.86138C11.9815 4.94487 12.0131 5.02432 12.0625 5.09251C12.3407 5.50984 12.4926 5.99853 12.5 6.50001V7.00001Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LinkChainIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.75027 5.52371L10.7168 4.55722C13.1264 2.14759 17.0332 2.14759 19.4428 4.55722C21.8524 6.96684 21.8524 10.8736 19.4428 13.2832L18.4742 14.2519M5.52886 9.74513L4.55722 10.7168C2.14759 13.1264 2.1476 17.0332 4.55722 19.4428C6.96684 21.8524 10.8736 21.8524 13.2832 19.4428L14.2478 18.4782M9.5 14.5L14.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const LinkIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.99634 4.3125C5.99634 4.00184 6.24818 3.75 6.55884 3.75H12.1875C13.3266 3.75 14.25 4.67341 14.25 5.8125V12.1844C14.25 12.495 13.9982 12.7469 13.6875 12.7469C13.3769 12.7469 13.125 12.495 13.125 12.1844V5.8125C13.125 5.76745 13.1218 5.72315 13.1157 5.6798L5.27275 13.5227C5.05308 13.7424 4.69692 13.7424 4.47725 13.5227C4.25758 13.3031 4.25758 12.9469 4.47725 12.7273L12.3202 4.88432C12.2768 4.87818 12.2326 4.875 12.1875 4.875H6.55884C6.24818 4.875 5.99634 4.62316 5.99634 4.3125Z"
|
||||
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,23 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const RefreshIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.7586 5.25C5.80171 4.1001 7.33209 3.375 9 3.375C12.1066 3.375 14.625 5.8934 14.625 9C14.625 9.23861 14.6102 9.47349 14.5815 9.70383C14.543 10.0121 14.7618 10.2932 15.07 10.3316C15.3783 10.3701 15.6594 10.1513 15.6978 9.84304C15.7323 9.56663 15.75 9.28526 15.75 9C15.75 5.27208 12.7279 2.25 9 2.25C7.10933 2.25 5.36655 3.02807 4.125 4.28342V2.8125C4.125 2.50184 3.87316 2.25 3.5625 2.25C3.25184 2.25 3 2.50184 3 2.8125L3 5.25C3 5.87132 3.50368 6.375 4.125 6.375H6.5625C6.87316 6.375 7.125 6.12316 7.125 5.8125C7.125 5.50184 6.87316 5.25 6.5625 5.25H4.7586Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M3.41855 8.29617C3.45699 7.98789 3.23825 7.70683 2.92997 7.66839C2.6217 7.62994 2.34063 7.84869 2.30219 8.15696C2.26773 8.43337 2.25 8.71474 2.25 9C2.25 12.7279 5.27208 15.75 9 15.75C10.8952 15.75 12.6418 14.9682 13.8839 13.7076V15.1875C13.8839 15.4982 14.1357 15.75 14.4464 15.75C14.757 15.75 15.0089 15.4982 15.0089 15.1875V12.75C15.0089 12.1287 14.5052 11.625 13.8839 11.625L11.4464 11.625C11.1357 11.625 10.8839 11.8768 10.8839 12.1875C10.8839 12.4982 11.1357 12.75 11.4464 12.75H13.2414C12.1983 13.8999 10.6679 14.625 9 14.625C5.8934 14.625 3.375 12.1066 3.375 9C3.375 8.76139 3.38982 8.52651 3.41855 8.29617Z"
|
||||
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 StorageIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M13 3.42576C13 4.39738 10.7614 5.18502 8 5.18502C5.23858 5.18502 3 4.39738 3 3.42576M13 3.42576C13 2.45415 10.7614 1.6665 8 1.6665C5.23858 1.6665 3 2.45415 3 3.42576M13 3.42576V12.5739C13 13.5455 10.7614 14.3332 8 14.3332C5.23858 14.3332 3 13.5455 3 12.5739V3.42576M13 7.92562C13 8.89723 10.7614 9.68488 8 9.68488C5.23858 9.68488 3 8.89723 3 7.92562"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const TrendingIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.875 9.79623L2.68636 7.88111C2.95597 7.24472 3.86788 7.28002 4.08767 7.93535L5.09939 10.9518C5.33069 11.6415 6.30748 11.6322 6.52567 10.9383L8.97436 3.15067C9.2032 2.42289 10.2437 2.46059 10.4194 3.20303L13.1643 14.7969C13.3368 15.5254 14.3502 15.5807 14.6007 14.8754L16.125 10.583"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
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>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const WarningTriangleIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.00004 6.75007V10.1251M7.71074 2.91845L2.09776 12.3585C1.50324 13.3583 2.22379 14.6251 3.38706 14.6251H14.613C15.7763 14.6251 16.4968 13.3583 15.9023 12.3585L10.2893 2.91844C9.70792 1.9406 8.29216 1.9406 7.71074 2.91845Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="9" cy="12" r="0.5625" fill="currentColor" />
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
@ -38,6 +38,25 @@ export * from './WarningDiamondIcon';
|
||||
export * from './ArrowRightCircleIcon';
|
||||
export * from './ClockOutlineIcon';
|
||||
export * from './ArrowRightCircleFilledIcon';
|
||||
export * from './GithubStrokeIcon';
|
||||
export * from './BranchStrokeIcon';
|
||||
export * from './StorageIcon';
|
||||
export * from './LinkIcon';
|
||||
export * from './LinkChainIcon';
|
||||
export * from './CursorBoxIcon';
|
||||
export * from './CrossCircleIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './CommitIcon';
|
||||
export * from './RocketIcon';
|
||||
export * from './RefreshIcon';
|
||||
export * from './UndoIcon';
|
||||
export * from './LoaderIcon';
|
||||
export * from './MinusCircleIcon';
|
||||
export * from './CopyIcon';
|
||||
export * from './CirclePlaceholderOnIcon';
|
||||
export * from './WarningTriangleIcon';
|
||||
export * from './CheckRadioOutlineIcon';
|
||||
export * from './TrendingIcon';
|
||||
|
||||
// Templates
|
||||
export * from './templates';
|
||||
|
@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const datePickerTheme = tv({
|
||||
slots: {
|
||||
input: [],
|
||||
input: ['w-full'],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Input, InputProps } from 'components/shared/Input';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { datePickerTheme } from './DatePicker.theme';
|
||||
import { Calendar, CalendarProps } from 'components/shared/Calendar';
|
||||
import { CalendarIcon } from 'components/shared/CustomIcon';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronGrabberHorizontal,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
@ -27,6 +30,10 @@ export interface DatePickerProps
|
||||
* Whether to allow the selection of a date range.
|
||||
*/
|
||||
selectRange?: boolean;
|
||||
/**
|
||||
* Optional callback function that is called when the date picker is reset.
|
||||
*/
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,6 +46,7 @@ export const DatePicker = ({
|
||||
calendarProps,
|
||||
value,
|
||||
onChange,
|
||||
onReset,
|
||||
selectRange = false,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
@ -50,15 +58,15 @@ export const DatePicker = ({
|
||||
* Renders the value of the date based on the current state of `props.value`.
|
||||
* @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
|
||||
*/
|
||||
const renderValue = useCallback(() => {
|
||||
if (!value) return undefined;
|
||||
const renderValue = useMemo(() => {
|
||||
if (!value) return '';
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((date) => format(date as Date, 'dd/MM/yyyy'))
|
||||
.join(' - ');
|
||||
}
|
||||
return format(value, 'dd/MM/yyyy');
|
||||
}, [value]);
|
||||
}, [value, onReset]);
|
||||
|
||||
/**
|
||||
* Handles the selection of a date from the calendar.
|
||||
@ -71,15 +79,21 @@ export const DatePicker = ({
|
||||
[setOpen, onChange],
|
||||
);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setOpen(false);
|
||||
onReset?.();
|
||||
}, [setOpen, onReset]);
|
||||
|
||||
return (
|
||||
<Popover.Root open={open}>
|
||||
<Popover.Trigger>
|
||||
<Popover.Trigger className="w-full">
|
||||
<Input
|
||||
{...props}
|
||||
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
|
||||
leftIcon={<CalendarIcon onClick={() => setOpen(true)} />}
|
||||
rightIcon={<ChevronGrabberHorizontal />}
|
||||
readOnly
|
||||
placeholder="Select a date..."
|
||||
value={renderValue()}
|
||||
value={renderValue}
|
||||
className={input({ className })}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
@ -93,6 +107,7 @@ export const DatePicker = ({
|
||||
{...calendarProps}
|
||||
selectRange={selectRange}
|
||||
value={value}
|
||||
onReset={handleReset}
|
||||
onCancel={() => setOpen(false)}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
|
@ -8,6 +8,8 @@ export const inputTheme = tv(
|
||||
'items-center',
|
||||
'rounded-lg',
|
||||
'relative',
|
||||
'gap-2',
|
||||
'w-full',
|
||||
'placeholder:text-elements-disabled',
|
||||
'disabled:cursor-not-allowed',
|
||||
'disabled:bg-controls-disabled',
|
||||
@ -27,7 +29,7 @@ export const inputTheme = tv(
|
||||
'disabled:shadow-none',
|
||||
'disabled:border-none',
|
||||
],
|
||||
icon: ['text-elements-mid-em'],
|
||||
icon: ['text-elements-low-em'],
|
||||
iconContainer: [
|
||||
'absolute',
|
||||
'inset-y-0',
|
||||
|
@ -47,15 +47,15 @@ export const Input = ({
|
||||
helperIcon: helperIconCls,
|
||||
} = inputTheme({ ...styleProps });
|
||||
|
||||
const renderLabels = useMemo(
|
||||
() => (
|
||||
<div className="space-y-1">
|
||||
const renderLabels = useMemo(() => {
|
||||
if (!label && !description) return null;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className={labelCls()}>{label}</p>
|
||||
<p className={descriptionCls()}>{description}</p>
|
||||
</div>
|
||||
),
|
||||
[labelCls, descriptionCls, label, description],
|
||||
);
|
||||
}, [labelCls, descriptionCls, label, description]);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
@ -73,8 +73,9 @@ export const Input = ({
|
||||
);
|
||||
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||
|
||||
const renderHelperText = useMemo(
|
||||
() => (
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={helperTextCls()}>
|
||||
{state &&
|
||||
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||
@ -82,17 +83,16 @@ export const Input = ({
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
),
|
||||
[cloneIcon, state, helperIconCls, helperText, helperTextCls],
|
||||
);
|
||||
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-y-2 w-full">
|
||||
{renderLabels}
|
||||
<div className={containerCls({ class: className })}>
|
||||
{leftIcon && renderLeftIcon}
|
||||
<input
|
||||
className={cn(inputCls({ class: 'w-80' }), {
|
||||
className={cn(inputCls(), {
|
||||
'pl-10': leftIcon,
|
||||
})}
|
||||
{...props}
|
||||
|
@ -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';
|
@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const radioTheme = tv({
|
||||
slots: {
|
||||
root: ['flex', 'gap-3', 'flex-wrap'],
|
||||
root: ['flex', 'gap-3'],
|
||||
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
|
||||
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
|
||||
radio: [
|
||||
@ -39,15 +39,34 @@ export const radioTheme = tv({
|
||||
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
|
||||
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
|
||||
],
|
||||
icon: ['w-[18px]', 'h-[18px]'],
|
||||
},
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: { root: ['flex-col'] },
|
||||
horizontal: { root: ['flex-row'] },
|
||||
},
|
||||
variant: {
|
||||
unstyled: {},
|
||||
card: {
|
||||
wrapper: [
|
||||
'px-4',
|
||||
'py-3',
|
||||
'rounded-lg',
|
||||
'border',
|
||||
'border-border-interactive',
|
||||
'bg-controls-tertiary',
|
||||
'shadow-button',
|
||||
'w-full',
|
||||
'cursor-pointer',
|
||||
],
|
||||
label: ['select-none', 'cursor-pointer'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'vertical',
|
||||
variant: 'unstyled',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -49,14 +49,15 @@ export const Radio = ({
|
||||
className,
|
||||
options,
|
||||
orientation,
|
||||
variant,
|
||||
...props
|
||||
}: RadioProps) => {
|
||||
const { root } = radioTheme({ orientation });
|
||||
const { root } = radioTheme({ orientation, variant });
|
||||
|
||||
return (
|
||||
<RadixRoot {...props} className={root({ className })}>
|
||||
{options.map((option) => (
|
||||
<RadioItem key={option.value} {...option} />
|
||||
<RadioItem key={option.value} variant={variant} {...option} />
|
||||
))}
|
||||
</RadixRoot>
|
||||
);
|
||||
|
@ -1,13 +1,16 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import React, { ReactNode, ComponentPropsWithoutRef } from 'react';
|
||||
import {
|
||||
Item as RadixRadio,
|
||||
Indicator as RadixIndicator,
|
||||
RadioGroupItemProps,
|
||||
RadioGroupIndicatorProps,
|
||||
} from '@radix-ui/react-radio-group';
|
||||
import { radioTheme } from './Radio.theme';
|
||||
import { RadioTheme, radioTheme } from './Radio.theme';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
export interface RadioItemProps extends RadioGroupItemProps {
|
||||
export interface RadioItemProps
|
||||
extends RadioGroupItemProps,
|
||||
Pick<RadioTheme, 'variant'> {
|
||||
/**
|
||||
* The wrapper props of the radio item.
|
||||
* You can use this prop to customize the wrapper props.
|
||||
@ -27,6 +30,10 @@ export interface RadioItemProps extends RadioGroupItemProps {
|
||||
* The id of the radio item.
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* The left icon of the radio item.
|
||||
*/
|
||||
leftIcon?: ReactNode;
|
||||
/**
|
||||
* The label of the radio item.
|
||||
*/
|
||||
@ -41,18 +48,29 @@ export const RadioItem = ({
|
||||
wrapperProps,
|
||||
labelProps,
|
||||
indicatorProps,
|
||||
leftIcon,
|
||||
label,
|
||||
id,
|
||||
variant,
|
||||
...props
|
||||
}: RadioItemProps) => {
|
||||
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
|
||||
const {
|
||||
wrapper,
|
||||
label: labelClass,
|
||||
radio,
|
||||
indicator,
|
||||
icon,
|
||||
} = radioTheme({ variant });
|
||||
|
||||
// Generate a unique id for the radio item from the label if the id is not provided
|
||||
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
|
||||
const componentId = id ?? kebabCaseLabel;
|
||||
|
||||
return (
|
||||
<div className={wrapper({ className: wrapperProps?.className })}>
|
||||
<label
|
||||
htmlFor={componentId}
|
||||
className={wrapper({ className: wrapperProps?.className })}
|
||||
>
|
||||
<RadixRadio {...props} className={radio({ className })} id={componentId}>
|
||||
<RadixIndicator
|
||||
forceMount
|
||||
@ -60,15 +78,20 @@ export const RadioItem = ({
|
||||
className={indicator({ className: indicatorProps?.className })}
|
||||
/>
|
||||
</RadixRadio>
|
||||
{leftIcon && (
|
||||
<span>
|
||||
{cloneIcon(leftIcon, { className: icon(), 'aria-hidden': true })}
|
||||
</span>
|
||||
)}
|
||||
{label && (
|
||||
<label
|
||||
{...labelProps}
|
||||
className={labelClass({ className: labelProps?.className })}
|
||||
htmlFor={componentId}
|
||||
className={labelClass({ className: labelProps?.className })}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const selectTheme = tv({
|
||||
slots: {
|
||||
container: ['flex', 'flex-col', 'relative', 'gap-2'],
|
||||
container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'],
|
||||
label: ['text-sm', 'text-elements-high-em'],
|
||||
description: ['text-xs', 'text-elements-low-em'],
|
||||
inputWrapper: [
|
||||
@ -85,7 +85,7 @@ export const selectTheme = tv({
|
||||
size: {
|
||||
md: {
|
||||
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]'],
|
||||
helperText: 'text-sm',
|
||||
helperIcon: ['h-5', 'w-5'],
|
||||
@ -93,7 +93,7 @@ export const selectTheme = tv({
|
||||
},
|
||||
sm: {
|
||||
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'],
|
||||
helperText: 'text-xs',
|
||||
helperIcon: ['h-4', 'w-4'],
|
||||
|
@ -3,7 +3,6 @@ import React, {
|
||||
useState,
|
||||
ComponentPropsWithoutRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
useRef,
|
||||
useEffect,
|
||||
@ -11,8 +10,8 @@ import React, {
|
||||
import { useMultipleSelection, useCombobox } from 'downshift';
|
||||
import { SelectTheme, selectTheme } from './Select.theme';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CrossIcon,
|
||||
ChevronGrabberHorizontal,
|
||||
CrossCircleIcon,
|
||||
WarningIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
@ -135,7 +134,9 @@ export const Select = ({
|
||||
const theme = selectTheme({ size, error, variant, orientation });
|
||||
|
||||
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 [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||||
'bottom',
|
||||
@ -166,23 +167,8 @@ export const Select = ({
|
||||
}
|
||||
}, [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 | undefined) => {
|
||||
if (!selectedItem) return;
|
||||
setSelectedItem(selectedItem);
|
||||
setInputValue(selectedItem ? selectedItem.label : '');
|
||||
onChange?.(selectedItem as SelectOption);
|
||||
@ -194,13 +180,13 @@ export const Select = ({
|
||||
addSelectedItem,
|
||||
removeSelectedItem,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
reset,
|
||||
} = useMultipleSelection<SelectOption>({
|
||||
selectedItems: multiple ? (value as SelectOption[]) : [],
|
||||
onSelectedItemsChange: multiple
|
||||
? undefined
|
||||
: ({ selectedItems }) => {
|
||||
handleSelectedItemChange(selectedItems?.[0] || null);
|
||||
handleSelectedItemChange(selectedItems?.[0]);
|
||||
},
|
||||
});
|
||||
|
||||
@ -234,6 +220,7 @@ export const Select = ({
|
||||
openMenu,
|
||||
} = useCombobox({
|
||||
items: filteredItems,
|
||||
selectedItem: multiple ? null : (value as SelectOption) || null,
|
||||
// @ts-expect-error – there are two params but we don't need the second one
|
||||
isItemDisabled: (item) => item.disabled,
|
||||
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
||||
@ -265,16 +252,12 @@ export const Select = ({
|
||||
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
|
||||
itemToString: (item) => (item && !multiple ? item.label : ''),
|
||||
});
|
||||
|
||||
const isSelected = useCallback(
|
||||
(item: SelectOption) =>
|
||||
multiple ? selectedItems.includes(item) : selectedItem === item,
|
||||
[selectedItems, selectedItem, multiple],
|
||||
);
|
||||
const isSelected = (item: SelectOption) =>
|
||||
multiple ? selectedItems.includes(item) : selectedItem === item;
|
||||
|
||||
const handleClear = (e: MouseEvent<SVGSVGElement, globalThis.MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
@ -284,29 +267,32 @@ export const Select = ({
|
||||
onClear?.();
|
||||
};
|
||||
|
||||
const renderLabels = useMemo(
|
||||
() => (
|
||||
<div className="space-y-1">
|
||||
const renderLabels = useMemo(() => {
|
||||
if (!label && !description) return null;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className={theme.label()}>{label}</p>
|
||||
<p className={theme.description()}>{description}</p>
|
||||
</div>
|
||||
),
|
||||
[theme, label, description],
|
||||
);
|
||||
}, [theme, label, description]);
|
||||
|
||||
const renderLeftIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={theme.iconContainer({ class: 'left-0 pl-4' })}>
|
||||
{cloneIcon(leftIcon, { className: theme.icon(), 'aria-hidden': true })}
|
||||
{cloneIcon(selectedItem?.leftIcon ? selectedItem.leftIcon : leftIcon, {
|
||||
className: theme.icon(),
|
||||
'aria-hidden': true,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, theme, leftIcon]);
|
||||
}, [cloneIcon, theme, leftIcon, selectedItem]);
|
||||
|
||||
const renderRightIcon = useMemo(() => {
|
||||
return (
|
||||
<div className={theme.iconContainer({ class: 'pr-4 right-0' })}>
|
||||
{clearable && (selectedItems.length > 0 || selectedItem) && (
|
||||
<CrossIcon
|
||||
<CrossCircleIcon
|
||||
className={theme.icon({ class: 'h-4 w-4' })}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
@ -314,14 +300,15 @@ export const Select = ({
|
||||
{rightIcon ? (
|
||||
cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true })
|
||||
) : (
|
||||
<ChevronDownIcon className={theme.icon()} />
|
||||
<ChevronGrabberHorizontal className={theme.icon()} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [cloneIcon, theme, rightIcon]);
|
||||
}, [cloneIcon, theme, rightIcon, selectedItem, selectedItems, clearable]);
|
||||
|
||||
const renderHelperText = useMemo(
|
||||
() => (
|
||||
const renderHelperText = useMemo(() => {
|
||||
if (!helperText) return null;
|
||||
return (
|
||||
<div className={theme.helperText()}>
|
||||
{error &&
|
||||
cloneIcon(<WarningIcon className={theme.helperIcon()} />, {
|
||||
@ -329,13 +316,13 @@ export const Select = ({
|
||||
})}
|
||||
<p>{helperText}</p>
|
||||
</div>
|
||||
),
|
||||
[cloneIcon, error, theme, helperText],
|
||||
);
|
||||
}, [cloneIcon, error, theme, helperText]);
|
||||
|
||||
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
||||
const isMultipleHasValueButNotSearchable =
|
||||
multiple && !searchable && selectedItems.length > 0;
|
||||
|
||||
const displayPlaceholder = useMemo(() => {
|
||||
if (hideValues && isMultipleHasValue) {
|
||||
return `${selectedItems.length} selected`;
|
||||
@ -360,7 +347,7 @@ export const Select = ({
|
||||
onClick={() => !dropdownOpen && openMenu()}
|
||||
>
|
||||
{/* Left icon */}
|
||||
{leftIcon && renderLeftIcon}
|
||||
{renderLeftIcon}
|
||||
|
||||
{/* Multiple input values */}
|
||||
{isMultipleHasValue &&
|
||||
@ -391,6 +378,8 @@ export const Select = ({
|
||||
'w-6': isMultipleHasValueButNotSearchable && !hideValues,
|
||||
// Add margin to the X icon
|
||||
'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';
|
@ -8,7 +8,6 @@ export const tabsTheme = tv({
|
||||
triggerWrapper: [
|
||||
// Horizontal – default
|
||||
'px-1',
|
||||
'pb-5',
|
||||
'cursor-default',
|
||||
'select-none',
|
||||
'text-elements-low-em',
|
||||
@ -55,9 +54,13 @@ export const tabsTheme = tv({
|
||||
'outline-none',
|
||||
'leading-none',
|
||||
'tracking-[-0.006em]',
|
||||
'text-sm',
|
||||
'rounded-md',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:focus-ring',
|
||||
'data-[orientation=horizontal]:h-10',
|
||||
// select direct child of data-[orientation=horizontal]
|
||||
'[&[data-orientation=horizontal]_>_*]:h-full',
|
||||
// Vertical
|
||||
'data-[orientation=vertical]:gap-2',
|
||||
'data-[orientation=vertical]:justify-start',
|
||||
@ -68,6 +71,7 @@ export const tabsTheme = tv({
|
||||
'gap-5',
|
||||
'border-b',
|
||||
'border-transparent',
|
||||
'overflow-scroll',
|
||||
// Horizontal – default
|
||||
'data-[orientation=horizontal]:border-border-interactive/10',
|
||||
// Vertical
|
||||
|
@ -4,26 +4,26 @@ import type { VariantProps } from 'tailwind-variants';
|
||||
export const tagTheme = tv(
|
||||
{
|
||||
slots: {
|
||||
wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
wrapper: ['inline-flex', 'gap-1.5', 'rounded-lg', 'border'],
|
||||
icon: [],
|
||||
label: ['font-inter', 'text-xs'],
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
attention: {
|
||||
icon: ['text-elements-warning'],
|
||||
wrapper: ['text-elements-warning'],
|
||||
},
|
||||
negative: {
|
||||
icon: ['text-elements-danger'],
|
||||
wrapper: ['text-elements-danger'],
|
||||
},
|
||||
positive: {
|
||||
icon: ['text-elements-success'],
|
||||
wrapper: ['text-elements-success'],
|
||||
},
|
||||
emphasized: {
|
||||
icon: ['text-elements-on-secondary'],
|
||||
wrapper: ['text-elements-on-secondary'],
|
||||
},
|
||||
neutral: {
|
||||
icon: ['text-elements-mid-em'],
|
||||
wrapper: ['text-elements-mid-em'],
|
||||
},
|
||||
},
|
||||
style: {
|
||||
@ -36,9 +36,11 @@ export const tagTheme = tv(
|
||||
size: {
|
||||
sm: {
|
||||
wrapper: ['px-2', 'py-2'],
|
||||
icon: ['h-4', 'w-4'],
|
||||
},
|
||||
xs: {
|
||||
wrapper: ['px-2', 'py-1.5'],
|
||||
wrapper: ['px-2', 'py-1'],
|
||||
icon: ['h-3', 'w-3'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -27,6 +27,8 @@ export const Tag = ({
|
||||
type = 'attention',
|
||||
style = 'default',
|
||||
size = 'sm',
|
||||
className,
|
||||
...props
|
||||
}: TagProps) => {
|
||||
const {
|
||||
wrapper: wrapperCls,
|
||||
@ -51,7 +53,7 @@ export const Tag = ({
|
||||
}, [cloneIcon, iconCls, rightIcon]);
|
||||
|
||||
return (
|
||||
<div className={wrapperCls()}>
|
||||
<div className={wrapperCls({ className })} {...props}>
|
||||
{renderLeftIcon}
|
||||
<p className={labelCls()}>{children}</p>
|
||||
{renderRightIcon}
|
||||
|
@ -32,7 +32,7 @@ const ProjectSearch = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-full">
|
||||
<div className="sticky top-0 bg-white z-30">
|
||||
<div className="flex pl-3 pr-8 pt-3 pb-3 items-center">
|
||||
<div className="grow">
|
||||
@ -64,7 +64,7 @@ const ProjectSearch = () => {
|
||||
</div>
|
||||
<HorizontalLine />
|
||||
</div>
|
||||
<div className="z-0">
|
||||
<div className="z-0 h-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
import { renderInputs } from './renders/input';
|
||||
import { RADIO_OPTIONS } from './renders/radio';
|
||||
import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls';
|
||||
import { renderHorizontalSteps, renderVerticalSteps } from './renders/steps';
|
||||
import {
|
||||
renderTabWithBadges,
|
||||
renderTabs,
|
||||
@ -56,6 +57,19 @@ const Page: React.FC = () => {
|
||||
|
||||
<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 */}
|
||||
<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,19 +7,15 @@ import {
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import { Project as ProjectType } from 'gql-client';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabsBody,
|
||||
TabsHeader,
|
||||
Typography,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import HorizontalLine from '../../../components/HorizontalLine';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { useOctokit } from '../../../context/OctokitContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { ChevronLeft } from 'components/shared/CustomIcon';
|
||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Tabs } from 'components/shared/Tabs';
|
||||
|
||||
const Id = () => {
|
||||
const { id } = useParams();
|
||||
@ -28,6 +24,9 @@ const Id = () => {
|
||||
const client = useGQLClient();
|
||||
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 [repoUrl, setRepoUrl] = useState('');
|
||||
|
||||
@ -69,96 +68,65 @@ const Id = () => {
|
||||
<div className="h-full">
|
||||
{project ? (
|
||||
<>
|
||||
<div className="flex p-4 gap-4 items-center">
|
||||
<div className="px-6 py-4 flex justify-between items-center gap-4">
|
||||
<div className="flex items-center justify-center gap-4 overflow-hidden">
|
||||
<Button
|
||||
variant="outlined"
|
||||
className="rounded-full"
|
||||
variant="tertiary"
|
||||
iconOnly
|
||||
className="rounded-full h-11 w-11 p-0 shrink-0"
|
||||
aria-label="Go back"
|
||||
leftIcon={<ChevronLeft />}
|
||||
onClick={() => navigate(-1)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'<'}
|
||||
</Button>
|
||||
<Typography variant="h3" className="grow" placeholder={''}>
|
||||
/>
|
||||
<Heading className="text-2xl font-medium truncate">
|
||||
{project?.name}
|
||||
</Typography>
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Link to={repoUrl} target="_blank">
|
||||
<Button
|
||||
className="rounded-full"
|
||||
variant="outlined"
|
||||
placeholder={''}
|
||||
{...buttonSize}
|
||||
className="h-11 transition-colors"
|
||||
variant="tertiary"
|
||||
>
|
||||
Open Repo
|
||||
Open repo
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="rounded-full" color="blue" placeholder={''}>
|
||||
<Button {...buttonSize} className="h-11 transition-colors">
|
||||
Go to app
|
||||
</Button>
|
||||
</div>
|
||||
<HorizontalLine />
|
||||
<div className="p-4">
|
||||
<Tabs value={currentTab}>
|
||||
<TabsHeader
|
||||
className="rounded-none border-b border-blue-gray-50 bg-transparent p-0"
|
||||
indicatorProps={{
|
||||
className:
|
||||
'bg-transparent border-b-2 border-gray-900 shadow-none rounded-none',
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
<Link to="">
|
||||
<Tab
|
||||
value=""
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Overview
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="deployments">
|
||||
<Tab
|
||||
value="deployments"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Deployments
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="database">
|
||||
<Tab
|
||||
value="database"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Database
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="integrations">
|
||||
<Tab
|
||||
value="integrations"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Integrations
|
||||
</Tab>
|
||||
</Link>
|
||||
<Link to="settings">
|
||||
<Tab
|
||||
value="settings"
|
||||
className={'p-2 cursor-pointer'}
|
||||
placeholder={''}
|
||||
>
|
||||
Settings
|
||||
</Tab>
|
||||
</Link>
|
||||
</TabsHeader>
|
||||
<TabsBody placeholder={''}>
|
||||
</div>
|
||||
<WavyBorder />
|
||||
<div className="px-6 h-full">
|
||||
<Tabs value={currentTab} className="flex-col pt-6 h-full">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="">
|
||||
<Link to="">Overview</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="deployments">
|
||||
<Link to="deployments">Deployments</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="integrations">
|
||||
<Link to="integrations">Integrations</Link>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">
|
||||
<Link to="settings">Settings</Link>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
{/* Not wrapping in Tab.Content because we are using Outlet */}
|
||||
<div className="py-7 h-full">
|
||||
<Outlet context={{ project, onUpdate }} />
|
||||
</TabsBody>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h4>Project not found</h4>
|
||||
<div className="grid place-items-center h-[calc(100vh-174px)]">
|
||||
<Heading as="h4" className="text-2xl font-medium">
|
||||
Project not found.
|
||||
</Heading>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -6,10 +6,14 @@ import {
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@material-tailwind/react';
|
||||
|
||||
import Stepper from '../../../../components/Stepper';
|
||||
import templates from '../../../../assets/templates';
|
||||
import {
|
||||
LinkChainIcon,
|
||||
TemplateIcon,
|
||||
TemplateIconType,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Steps } from 'components/shared/Steps';
|
||||
|
||||
// TODO: Set dynamic route for template and load details from DB
|
||||
const CreateWithTemplate = () => {
|
||||
@ -44,25 +48,30 @@ const CreateWithTemplate = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6 items-center">
|
||||
<Avatar variant="rounded" src="/gray.png" placeholder={''} />
|
||||
<div className="grow px-2">{template?.name}</div>
|
||||
<div className="flex flex-col lg:flex-row justify-between w-5/6 my-4 bg-base-bg-alternate rounded-xl p-6 gap-3 items-start lg:items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<TemplateIcon type={template?.icon as TemplateIconType} size={48} />
|
||||
<Heading className="font-medium">{template?.name}</Heading>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={`https://github.com/${template?.repoFullName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex gap-1.5 items-center text-sm"
|
||||
>
|
||||
^{' '}
|
||||
<LinkChainIcon size={18} />
|
||||
<span className="underline">
|
||||
{Boolean(template?.repoFullName)
|
||||
? template?.repoFullName
|
||||
: 'Template not supported'}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 w-5/6 p-6">
|
||||
<div>
|
||||
<Stepper activeStep={activeStep} stepperValues={stepperValues} />
|
||||
<Steps currentIndex={activeStep} steps={stepperValues} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Outlet context={{ template }} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
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 { useOctokit } from 'context/OctokitContext';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
@ -13,8 +13,8 @@ const NewProject = () => {
|
||||
return isAuth ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<Heading as="h3" className="font-medium text-lg">
|
||||
Start with template
|
||||
<Heading as="h3" className="font-medium text-lg pl-1">
|
||||
Start with a template
|
||||
</Heading>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{templates.map((template) => {
|
||||
@ -28,7 +28,7 @@ const NewProject = () => {
|
||||
})}
|
||||
</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
|
||||
</Heading>
|
||||
<RepositoryList octokit={octokit} />
|
||||
|
@ -6,6 +6,7 @@ import { WavyBorder } from 'components/shared/WavyBorder';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon } from 'components/shared/CustomIcon';
|
||||
import { cn } from 'utils/classnames';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
|
||||
export interface CreateProjectLayoutProps
|
||||
extends ComponentPropsWithoutRef<'section'> {}
|
||||
@ -16,24 +17,77 @@ export const CreateProjectLayout = ({
|
||||
}: CreateProjectLayoutProps) => {
|
||||
const { orgSlug } = useParams();
|
||||
|
||||
return (
|
||||
<section {...props} className={cn('h-full flex flex-col', className)}>
|
||||
<div className="sticky top-0">
|
||||
<div className="flex px-6 py-4 bg-base-bg items-center gap-4">
|
||||
<Heading as="h2" className="flex-1 text-[24px] font-medium">
|
||||
const closeBtnLink = `/${orgSlug}`;
|
||||
|
||||
const heading = (
|
||||
<Heading as="h2" className="flex-1 text-xl md:text-2xl font-medium">
|
||||
Create new project
|
||||
</Heading>
|
||||
<Link to={`/${orgSlug}`}>
|
||||
<Button iconOnly variant="tertiary">
|
||||
<CrossIcon size={18} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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>
|
||||
</div>
|
||||
<WavyBorder />
|
||||
</div>
|
||||
|
||||
<section className="px-6 h-full flex-1 py-6 overflow-y-auto">
|
||||
<Outlet />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +1,18 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
|
||||
import { useForm, SubmitHandler, Controller } from 'react-hook-form';
|
||||
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
|
||||
import toast from 'react-hot-toast';
|
||||
import assert from 'assert';
|
||||
|
||||
import { Button, Option, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import AsyncSelect from '../../../../../components/shared/AsyncSelect';
|
||||
import { Template } from '../../../../../types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
type SubmitRepoValues = {
|
||||
framework: string;
|
||||
@ -93,7 +96,7 @@ const CreateRepo = () => {
|
||||
fetchUserAndOrgs();
|
||||
}, [octokit]);
|
||||
|
||||
const { register, handleSubmit, control, reset } = useForm<SubmitRepoValues>({
|
||||
const { handleSubmit, control, reset } = useForm<SubmitRepoValues>({
|
||||
defaultValues: {
|
||||
framework: 'React',
|
||||
repoName: '',
|
||||
@ -110,87 +113,68 @@ const CreateRepo = () => {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(submitRepoHandler)}>
|
||||
<div className="mb-2">
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
Create a repository
|
||||
</Typography>
|
||||
<Typography color="gray" placeholder={''}>
|
||||
The project will be cloned into this repository
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<h5>Framework</h5>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="inline-flex items-center w-1/2 border rounded-lg p-2">
|
||||
<input
|
||||
type="radio"
|
||||
{...register('framework')}
|
||||
value="React"
|
||||
className="h-5 w-5 text-indigo-600 rounded"
|
||||
/>
|
||||
<span className="ml-2">^React</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center w-1/2 border rounded-lg p-2">
|
||||
<input
|
||||
type="radio"
|
||||
{...register('framework')}
|
||||
className="h-5 w-5 text-indigo-600 rounded"
|
||||
value="Next"
|
||||
/>
|
||||
<span className="ml-2">^Next</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<h5>Git account</h5>
|
||||
<div className="flex flex-col gap-4 lg:gap-7 w-full">
|
||||
<div>
|
||||
<Heading as="h3" className="text-lg font-medium">
|
||||
Create a repository
|
||||
</Heading>
|
||||
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
|
||||
The project will be cloned into this repository
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-3">
|
||||
<span className="text-sm text-elements-high-em">Git account</span>
|
||||
<Controller
|
||||
name="account"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<AsyncSelect {...field}>
|
||||
{gitAccounts.map((account, key) => (
|
||||
<Option key={key} value={account}>
|
||||
^ {account}
|
||||
</Option>
|
||||
))}
|
||||
</AsyncSelect>
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
value={{ value } as SelectOption}
|
||||
onChange={(value) => onChange((value as SelectOption).value)}
|
||||
options={
|
||||
gitAccounts.map((account) => ({
|
||||
value: account,
|
||||
label: account,
|
||||
})) ?? []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-3">
|
||||
<span className="text-sm text-elements-high-em">Name the repo</span>
|
||||
<Controller
|
||||
name="repoName"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<h5>Name the repo</h5>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded p-2 w-full focus:border-blue-300 focus:outline-none focus:shadow-outline-blue"
|
||||
placeholder=""
|
||||
{...register('repoName')}
|
||||
<Controller
|
||||
name="isPrivate"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label="Make this repo private"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="inline-flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-5 w-5 text-indigo-600 rounded"
|
||||
{...register('isPrivate')}
|
||||
/>
|
||||
<span className="ml-2">Make this repo private</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div>
|
||||
<Button
|
||||
className="bg-blue-500 rounded-xl p-2"
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={!Boolean(template.repoFullName) || isLoading}
|
||||
loading={isLoading}
|
||||
placeholder={''}
|
||||
rightIcon={<ArrowRightCircleFilledIcon />}
|
||||
>
|
||||
Deploy ^
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -2,19 +2,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Deployment, Domain } from 'gql-client';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import DeploymentDetailsCard from '../../../../components/projects/project/deployments/DeploymentDetailsCard';
|
||||
import DeploymentDetailsCard from 'components/projects/project/deployments/DeploymentDetailsCard';
|
||||
import FilterForm, {
|
||||
FilterValue,
|
||||
StatusOptions,
|
||||
} from '../../../../components/projects/project/deployments/FilterForm';
|
||||
import { OutletContextType } from '../../../../types';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
} from 'components/projects/project/deployments/FilterForm';
|
||||
import { OutletContextType } from 'types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { RefreshIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||
searchedBranch: '',
|
||||
status: StatusOptions.ALL_STATUS,
|
||||
status: '',
|
||||
};
|
||||
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
||||
|
||||
@ -73,12 +73,19 @@ const DeploymentsTabPanel = () => {
|
||||
// TODO: match status field types
|
||||
(deployment.status as unknown as StatusOptions) === filterValue.status;
|
||||
|
||||
const startDate =
|
||||
filterValue.updateAtRange instanceof Array
|
||||
? filterValue.updateAtRange[0]
|
||||
: null;
|
||||
const endDate =
|
||||
filterValue.updateAtRange instanceof Array
|
||||
? filterValue.updateAtRange[1]
|
||||
: null;
|
||||
|
||||
const dateMatch =
|
||||
!filterValue.updateAtRange ||
|
||||
(new Date(Number(deployment.createdAt)) >=
|
||||
filterValue.updateAtRange!.from! &&
|
||||
new Date(Number(deployment.createdAt)) <=
|
||||
filterValue.updateAtRange!.to!);
|
||||
(new Date(Number(deployment.createdAt)) >= startDate! &&
|
||||
new Date(Number(deployment.createdAt)) <= endDate!);
|
||||
|
||||
return branchMatch && statusMatch && dateMatch;
|
||||
});
|
||||
@ -88,17 +95,17 @@ const DeploymentsTabPanel = () => {
|
||||
setFilterValue(DEFAULT_FILTER_VALUE);
|
||||
}, []);
|
||||
|
||||
const onUpdateDeploymenToProd = async () => {
|
||||
const onUpdateDeploymentToProd = async () => {
|
||||
await fetchDeployments();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<section className="h-full">
|
||||
<FilterForm
|
||||
value={filterValue}
|
||||
onChange={(value) => setFilterValue(value)}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 h-full">
|
||||
{Boolean(filteredDeployments.length) ? (
|
||||
filteredDeployments.map((deployment, key) => {
|
||||
return (
|
||||
@ -106,34 +113,34 @@ const DeploymentsTabPanel = () => {
|
||||
deployment={deployment}
|
||||
key={key}
|
||||
currentDeployment={currentDeployment!}
|
||||
onUpdate={onUpdateDeploymenToProd}
|
||||
onUpdate={onUpdateDeploymentToProd}
|
||||
project={project}
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="h-[50vh] bg-gray-100 flex rounded items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Typography variant="h5" placeholder={''}>
|
||||
// TODO: Update the height based on the layout, need to re-styling the layout similar to create project layout
|
||||
<div className="h-3/4 bg-base-bg-alternate flex flex-col rounded-xl items-center justify-center text-center gap-5">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium tracking-[-0.011em] text-elements-high-em">
|
||||
No deployments found
|
||||
</Typography>
|
||||
<Typography placeholder={''}>
|
||||
Please change your search query or filters
|
||||
</Typography>
|
||||
<Button
|
||||
className="rounded-full mt-5"
|
||||
color="white"
|
||||
onClick={handleResetFilters}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Reset filters
|
||||
</Button>
|
||||
</p>
|
||||
<p className="text-sm tracking-[-0.006em] text-elements-mid-em">
|
||||
Please change your search query or filters.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
leftIcon={<RefreshIcon />}
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,18 +1,30 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Domain, DomainStatus } from 'gql-client';
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { RequestError } from 'octokit';
|
||||
|
||||
import { Typography, Chip, Avatar, Tooltip } from '@material-tailwind/react';
|
||||
|
||||
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
||||
import { relativeTimeMs } from '../../../../utils/time';
|
||||
import { useOctokit } from '../../../../context/OctokitContext';
|
||||
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
CheckRoundFilledIcon,
|
||||
ClockIcon,
|
||||
CursorBoxIcon,
|
||||
GithubStrokeIcon,
|
||||
GlobeIcon,
|
||||
LinkIcon,
|
||||
StorageIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { Activity } from 'components/projects/project/overview/Activity';
|
||||
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
|
||||
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
|
||||
const COMMITS_PER_PAGE = 4;
|
||||
|
||||
@ -103,92 +115,109 @@ const OverviewTabPanel = () => {
|
||||
}, [project]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-[72px] mt-7">
|
||||
<div className="col-span-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-5 gap-6 md:gap-[72px]">
|
||||
<div className="col-span-5 md:col-span-3">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Avatar
|
||||
src={project.icon || '/gray.png'}
|
||||
variant="rounded"
|
||||
placeholder={''}
|
||||
size={48}
|
||||
initials={getInitials(project.name)}
|
||||
imageSrc={project.icon}
|
||||
type="blue"
|
||||
/>
|
||||
<div className="grow">
|
||||
<Typography placeholder={''}>{project.name}</Typography>
|
||||
<Typography variant="small" color="gray" placeholder={''}>
|
||||
<div className="flex-1 space-y-1 overflow-hidden">
|
||||
<Heading className="text-lg leading-6 font-medium truncate">
|
||||
{project.name}
|
||||
</Heading>
|
||||
<p className="text-sm text-elements-low-em tracking-tight truncate">
|
||||
{project.subDomain}
|
||||
</Typography>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm items-center">
|
||||
<div>^ Domain</div>
|
||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||
{liveDomain ? (
|
||||
<Chip
|
||||
className="normal-case ml-6 inline font-normal"
|
||||
size="sm"
|
||||
value="Connected"
|
||||
icon="^"
|
||||
color="green"
|
||||
/>
|
||||
<Tag type="positive" size="xs" leftIcon={<CheckRoundFilledIcon />}>
|
||||
Connected
|
||||
</Tag>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Chip
|
||||
className="normal-case inline font-normal mx-2"
|
||||
size="sm"
|
||||
value="Not connected"
|
||||
icon="^"
|
||||
color="orange"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tag type="attention" size="xs" leftIcon={<ClockIcon />}>
|
||||
Not connected
|
||||
</Tag>
|
||||
<Button
|
||||
className="normal-case rounded-full"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate('settings/domains');
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
>
|
||||
Setup
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
{project.deployments.length !== 0 ? (
|
||||
<>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Source</p>
|
||||
<p>^ {project.deployments[0]?.branch}</p>
|
||||
{/* SOURCE */}
|
||||
<OverviewInfo label="Source" icon={<GithubStrokeIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<BranchStrokeIcon className="text-elements-low-em w-4 h-5" />
|
||||
<span className="text-elements-high-em text-sm tracking-tighter">
|
||||
{project.deployments[0]?.branch}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Deployment</p>
|
||||
<p className="text-blue-600">{liveDomain?.name}</p>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DATABASE */}
|
||||
<OverviewInfo label="Database" icon={<StorageIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="#">
|
||||
<span className="group text-controls-primary hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||
{/* // TODO: add db name
|
||||
dbname
|
||||
*/}
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 text-sm">
|
||||
<p>^ Created</p>
|
||||
<p>
|
||||
{relativeTimeMs(project.deployments[0].createdAt)} by ^{' '}
|
||||
<Tooltip content={project.deployments[0].createdBy.name}>
|
||||
{formatAddress(project.deployments[0].createdBy.name ?? '')}
|
||||
</Tooltip>
|
||||
</p>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DEPLOYMENT */}
|
||||
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="#">
|
||||
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||
{liveDomain?.name}{' '}
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DEPLOYMENT DATE */}
|
||||
<OverviewInfo label="Deployment date" icon={<CalendarDaysIcon />}>
|
||||
<div className="flex gap-2 items-center text-elements-high-em text-sm tracking-tighter">
|
||||
<span>{relativeTimeMs(project.deployments[0].createdAt)}</span>
|
||||
by
|
||||
<Avatar
|
||||
// TODO: add imageSrc
|
||||
// imageSrc={project.deployments[0]?.createdBy.avatar}
|
||||
initials={getInitials(
|
||||
project.deployments[0]?.createdBy?.name ?? '',
|
||||
)}
|
||||
className="rounded-full"
|
||||
size={24}
|
||||
/>
|
||||
<span>{project.deployments[0]?.createdBy?.name}</span>
|
||||
</div>
|
||||
</OverviewInfo>
|
||||
</>
|
||||
) : (
|
||||
<div>No current deployment found</div>
|
||||
<p className="text-elements-low-em text-sm py-3">
|
||||
No current deployment found.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-2 mr-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading className="text-lg leading-6 font-medium">Activity</Heading>
|
||||
<Button variant="tertiary" size="sm">
|
||||
See all
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
{activities.map((activity, index) => {
|
||||
return (
|
||||
<ActivityCard activity={activity} key={`activity-${index}`} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Activity activities={activities} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
43
packages/frontend/src/utils/cloneElement.tsx
Normal file
43
packages/frontend/src/utils/cloneElement.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
isValidElement,
|
||||
Children,
|
||||
cloneElement as reactCloneElement,
|
||||
HTMLProps,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { ClassProp } from 'tailwind-variants';
|
||||
import { cn } from './classnames';
|
||||
|
||||
interface cloneElement extends HTMLProps<ReactNode> {
|
||||
element: ReactNode;
|
||||
themeStyle?: (props: ClassProp) => string;
|
||||
}
|
||||
|
||||
export const cloneElement = ({
|
||||
element,
|
||||
themeStyle,
|
||||
className,
|
||||
...props
|
||||
}: cloneElement) => {
|
||||
if (isValidElement(element)) {
|
||||
return (
|
||||
<>
|
||||
{Children.map(element, (child) => {
|
||||
const originalClassName = (child.props as HTMLProps<ReactNode>)
|
||||
?.className;
|
||||
|
||||
return reactCloneElement(child as ReactElement, {
|
||||
className: themeStyle
|
||||
? themeStyle({
|
||||
className: cn(originalClassName, className), // overriding icon classNames
|
||||
})
|
||||
: originalClassName,
|
||||
...props,
|
||||
});
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
@ -12,6 +12,9 @@ export default withMT({
|
||||
zIndex: {
|
||||
tooltip: '52',
|
||||
},
|
||||
letterSpacing: {
|
||||
tight: '-0.084px',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
display: ['Inter Display', 'sans-serif'],
|
||||
@ -84,6 +87,7 @@ export default withMT({
|
||||
900: '#0a3a5c',
|
||||
},
|
||||
base: {
|
||||
canvas: '#ECF6FE',
|
||||
bg: '#ffffff',
|
||||
'bg-alternate': '#f8fafc',
|
||||
'bg-emphasized': '#f1f5f9',
|
||||
|
23
yarn.lock
23
yarn.lock
@ -3786,6 +3786,27 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "1.0.1"
|
||||
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"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
usehooks-ts@^2.10.0:
|
||||
usehooks-ts@^2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.15.1.tgz#ede348c6f01b4b4fe981e240551624885a2fed83"
|
||||
integrity sha512-AK29ODCt4FT9XleILNbkbjjmkRCNaQrgxQEkvqHjlnT76iPXzTFGvK2Y/s83JEdSxRp43YEnSa3bYBEV6HZ26Q==
|
||||
|
Loading…
Reference in New Issue
Block a user