diff --git a/.vscode/settings.json b/.vscode/settings.json index f3e08235..c831fde1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { // IntelliSense for taiwind variants "tailwindCSS.experimental.classRegex": [ - ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] + "tv\\('([^)]*)\\')", + "(?:'|\"|`)([^\"'`]*)(?:'|\"|`)" ] } diff --git a/build-webapp.sh b/build-webapp.sh index b22dde5d..a77ec641 100755 --- a/build-webapp.sh +++ b/build-webapp.sh @@ -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 < deployments.some( (deployment) => - deployment.applicationRecordId === record.attributes.application + deployment.applicationRecordId === record.attributes.application && + record.attributes.url.includes(deployment.id) ) ); } diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index d204537c..5211585c 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -382,8 +382,7 @@ export class Service { async createDeployment ( userId: string, octokit: Octokit, - data: DeepPartial, - recordData: { repoUrl?: string } = {} + data: DeepPartial ): Promise { 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({ - owner, - repo - }); - recordData.repoUrl = repoDetails.html_url; - } + const repoUrl = (await octokit.rest.repos.get({ + owner, + repo + })).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 { - 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 } }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index eae6929f..80941346 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -24,6 +24,7 @@ export interface GitPushEventPayload { id: string; message: string; }; + deleted: boolean; } export interface AppDeploymentRecordAttributes { diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 693223bb..8d2a8e8b 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -66,3 +66,5 @@ export const loadAndSaveData = async ( return savedEntity; }; + +export const sleep = async (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/packages/deployer/README.md b/packages/deployer/README.md index 04c5d6b5..d2976852 100644 --- a/packages/deployer/README.md +++ b/packages/deployer/README.md @@ -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 - - ``: 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: - ... - ``` - -- Update commit hash in the following places: - - [records/application-record.yml](records/application-record.yml) - ```yml - record: - ... - repository_ref: - ... - ``` - - [records/application-deployment-request.yml](records/application-deployment-request.yml) - ```yml - record: - ... - meta: - ... - repository_ref: - ``` - - [deploy-frontend.sh](deploy-frontend.sh) - Also be sure to update the app version - ```bash - ... - RCD_APP_VERSION="" - REPO_REF="" - ... - ``` - - Run script to deploy app ``` ./deploy-frontend.sh diff --git a/packages/deployer/config.yml b/packages/deployer/config.yml index 2c6410df..4eeaf9a5 100644 --- a/packages/deployer/config.yml +++ b/packages/deployer/config.yml @@ -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 diff --git a/packages/deployer/deploy-frontend.sh b/packages/deployer/deploy-frontend.sh index 1e56f47a..92510428 100755 --- a/packages/deployer/deploy-frontend.sh +++ b/packages/deployer/deploy-frontend.sh @@ -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 < ./records/application-record.yml < { +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 ( -
+
{formatTime.days !== 0 && {formatTime.days}d } {formatTime.hours !== 0 && {formatTime.hours}h } {formatTime.minutes !== 0 && {formatTime.minutes}m } diff --git a/packages/frontend/src/components/StopWatch.tsx b/packages/frontend/src/components/StopWatch.tsx index 4e70f9b0..239b1197 100644 --- a/packages/frontend/src/components/StopWatch.tsx +++ b/packages/frontend/src/components/StopWatch.tsx @@ -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 { + offsetTimestamp: Date; +} + +const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => { const { totalSeconds } = useStopwatch({ autoStart: true, offsetTimestamp: offsetTimestamp, }); - return ; + return ; }; export { Stopwatch, setStopWatchOffset }; diff --git a/packages/frontend/src/components/projects/create/Deploy.tsx b/packages/frontend/src/components/projects/create/Deploy.tsx index 67698959..a251773d 100644 --- a/packages/frontend/src/components/projects/create/Deploy.tsx +++ b/packages/frontend/src/components/projects/create/Deploy.tsx @@ -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 ( -
-
-
-

Deployment started ...

-
- ^  +
+
+
+ + Deployment started ... + +
+
-
- -
+ {
- - - - + +
+ + + + +
); }; diff --git a/packages/frontend/src/components/projects/create/DeployStep.tsx b/packages/frontend/src/components/projects/create/DeployStep.tsx index 1aae9c0c..de78f789 100644 --- a/packages/frontend/src/components/projects/create/DeployStep.tsx +++ b/packages/frontend/src/components/projects/create/DeployStep.tsx @@ -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 ( -
-
- {status === DeployStatus.NOT_STARTED &&
{step}
} - {status === DeployStatus.PROCESSING &&
O
} - {status === DeployStatus.COMPLETE && ( -
- +
+ {/* Collapisble trigger */} +
- -
+ + + {/* Collapsible */} + +
+ {/* Logs */} {processLogs.map((log, key) => { return ( - +

{log} - +

); })} -
+ + {/* End of logs ref used for hiding gradient overlay */} +
+ + {/* Overflow gradient overlay */} + {!hideGradientOverlay && ( +
+ )} + + {/* Copy log button */} +
diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx b/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx deleted file mode 100644 index fbcf50b4..00000000 --- a/packages/frontend/src/components/projects/create/ProjectRepoCard.tsx +++ /dev/null @@ -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 = ({ 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 ( -
-
- -
-
-
- {repository.full_name} - {repository.visibility === 'private' && ( - } - /> - )} -
-

{repository.updated_at && relativeTimeISO(repository.updated_at)}

-
- {isLoading ? ( - - ) : ( -
- - {'>'} - -
- )} -
- ); -}; - -export default ProjectRepoCard; diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx b/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx new file mode 100644 index 00000000..b06fc233 --- /dev/null +++ b/packages/frontend/src/components/projects/create/ProjectRepoCard/ProjectRepoCard.tsx @@ -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 = ({ + 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 ( +
+ {/* Icon container */} +
+ +
+ {/* Content */} +
+
+

+ {repository.full_name} +

+

+ {repository.updated_at && relativeTimeISO(repository.updated_at)} +

+
+ {repository.visibility === 'private' && ( +
+ + Private +
+ )} +
+ {/* Right action */} + {isLoading ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts b/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts new file mode 100644 index 00000000..78472497 --- /dev/null +++ b/packages/frontend/src/components/projects/create/ProjectRepoCard/index.ts @@ -0,0 +1 @@ +export * from './ProjectRepoCard'; diff --git a/packages/frontend/src/components/projects/create/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx similarity index 58% rename from packages/frontend/src/components/projects/create/RepositoryList.tsx rename to packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx index f09e61d6..14af57e6 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx @@ -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(); const [orgs, setOrgs] = useState([]); // TODO: Add new type for Git user when required const [gitUser, setGitUser] = useState(); @@ -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: , + })); + }, [accounts]); + return ( -
-
-
- + {/* Dropdown and search */} +
+
+ setSearchedRepo(event.target.value)} placeholder="Search for repository" + leftIcon={} + onChange={(e) => setSearchedRepo(e.target.value)} />
+ + {/* Repository list */} {Boolean(repositoryDetails.length) ? ( - repositoryDetails.map((repo, key) => { - return ; - }) +
+ {repositoryDetails.map((repo, index) => ( + <> + + {/* Horizontal line */} + {index !== repositoryDetails.length - 1 && ( +
+ )} + + ))} +
) : (
@@ -151,8 +174,6 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
)} -
+ ); }; - -export default RepositoryList; diff --git a/packages/frontend/src/components/projects/create/RepositoryList/index.ts b/packages/frontend/src/components/projects/create/RepositoryList/index.ts new file mode 100644 index 00000000..dc3bc8c5 --- /dev/null +++ b/packages/frontend/src/components/projects/create/RepositoryList/index.ts @@ -0,0 +1 @@ +export * from './RepositoryList'; diff --git a/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx index b7c549fd..afae4827 100644 --- a/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx +++ b/packages/frontend/src/components/projects/create/TemplateCard/TemplateCard.tsx @@ -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 = ({ + 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 ( - )} - +
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index e191fe8c..c4940bac 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -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 ; } - }; - - 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 ; } - }; - 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 ; } }; return ( -
-
-
- {deployment.url && ( - +
+
+ {/* DEPLOYMENT URL */} + {deployment.url && ( + + {deployment.url} - - )} -
- + + + )} + {deployment.environment === Environment.Production ? `Production ${deployment.isCurrent ? '(Current)' : ''}` : 'Preview'} - +
-
- ^} + + {/* DEPLOYMENT STATUS */} +
+ + {deployment.status} + +
+ + {/* DEPLOYMENT COMMIT DETAILS */} +
+ + + {deployment.branch} + + + + + {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} + {deployment.commitMessage} + + +
+ + {/* DEPLOYMENT INFOs */} +
+
+ + + {relativeTimeMs(deployment.createdAt)} + +
+ +
+ + + {formatAddress(deployment.createdBy.name ?? '')} + +
+
-
- - ^ {deployment.branch} - - - ^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} - {deployment.commitMessage} - -
-
- - ^ {relativeTimeMs(deployment.createdAt)} ^{' '} - - {formatAddress(deployment.createdBy.name ?? '')} - - - - - - - - - - ^ Visit - - - setAssignDomainDialog(!assignDomainDialog)} - placeholder={''} - > - ^ Assign domain - - setChangeToProduction(!changeToProduction)} - disabled={!(deployment.environment !== Environment.Production)} - placeholder={''} - > - ^ Change to production - -
- setRedeployToProduction(!redeployToProduction)} - disabled={ - !( - deployment.environment === Environment.Production && - deployment.isCurrent - ) - } - placeholder={''} - > - ^ Redeploy to production - - setRollbackDeployment(!rollbackDeployment)} - disabled={ - deployment.isCurrent || - deployment.environment !== Environment.Production || - !Boolean(currentDeployment) - } - placeholder={''} - > - ^ Rollback to this version - -
-
-
- setChangeToProduction((preVal) => !preVal)} - open={changeToProduction} - confirmButtonTitle="Change" - color="blue" - handleConfirm={async () => { - await updateDeployment(); - setChangeToProduction((preVal) => !preVal); - }} - > -
- - Upon confirmation, this deployment will be changed to production. - - - - The new deployment will be associated with these domains: - - {prodBranchDomains.length > 0 && - prodBranchDomains.map((value) => { - return ( - - ^ {value.name} - - ); - })} -
-
- setRedeployToProduction((preVal) => !preVal)} - open={redeployToProduction} - confirmButtonTitle="Redeploy" - color="blue" - handleConfirm={async () => { - await redeployToProd(); - setRedeployToProduction((preVal) => !preVal); - }} - > -
- - Upon confirmation, new deployment will be created with the same - source code as current deployment. - - - - These domains will point to your new deployment: - - {deployment.domain?.name && ( - - {deployment.domain?.name} - - )} -
-
- {Boolean(currentDeployment) && ( - setRollbackDeployment((preVal) => !preVal)} - open={rollbackDeployment} - confirmButtonTitle="Rollback" - color="blue" - handleConfirm={async () => { - await rollbackDeploymentHandler(); - setRollbackDeployment((preVal) => !preVal); - }} - > -
- - Upon confirmation, this deployment will replace your current - deployment - - - - - These domains will point to your new deployment: - - - ^ {currentDeployment.domain?.name} - -
-
- )} - setAssignDomainDialog(!assignDomainDialog)} - />
); }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx new file mode 100644 index 00000000..f4eb9808 --- /dev/null +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -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; + 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 ( + <> +
+ + + +
+ {/* Dialogs */} + setChangeToProduction((preVal) => !preVal)} + open={changeToProduction} + confirmButtonTitle="Change" + color="blue" + handleConfirm={async () => { + await updateDeployment(); + setChangeToProduction((preVal) => !preVal); + }} + > +
+ + Upon confirmation, this deployment will be changed to production. + + + + The new deployment will be associated with these domains: + + {prodBranchDomains.length > 0 && + prodBranchDomains.map((value) => { + return ( + + ^ {value.name} + + ); + })} +
+
+ setRedeployToProduction((preVal) => !preVal)} + open={redeployToProduction} + confirmButtonTitle="Redeploy" + color="blue" + handleConfirm={async () => { + await redeployToProd(); + setRedeployToProduction((preVal) => !preVal); + }} + > +
+ + Upon confirmation, new deployment will be created with the same + source code as current deployment. + + + + These domains will point to your new deployment: + + {deployment.domain?.name && ( + + {deployment.domain?.name} + + )} +
+
+ {Boolean(currentDeployment) && ( + setRollbackDeployment((preVal) => !preVal)} + open={rollbackDeployment} + confirmButtonTitle="Rollback" + color="blue" + handleConfirm={async () => { + await rollbackDeploymentHandler(); + setRollbackDeployment((preVal) => !preVal); + }} + > +
+ + Upon confirmation, this deployment will replace your current + deployment + + + + + These domains will point to your new deployment: + + + ^ {currentDeployment.domain?.name} + +
+
+ )} + setAssignDomainDialog(!assignDomainDialog)} + /> + + ); +}; diff --git a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx index c28c6218..3668b7ac 100644 --- a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx +++ b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx @@ -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(); + const [dateRange, setDateRange] = useState(); 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 ; + case StatusOptions.READY: + return ; + case StatusOptions.ERROR: + return ; + case StatusOptions.ALL_STATUS: + default: + return ; + } + }; + + const statusOptions = Object.values(StatusOptions).map((status) => ({ + label: status, + value: status, + leftIcon: getOptionIcon(status), + })); + + const handleReset = () => { + setSearchedBranch(''); + }; + return ( -
-
- +
+ } + rightIcon={ + searchedBranch && + } value={searchedBranch} - onChange={(event) => setSearchedBranch(event.target.value)} + onChange={(e) => setSearchedBranch(e.target.value)} />
-
- +
+ setDateRange(undefined)} + />
-
+
- {selectedStatus !== StatusOptions.ALL_STATUS && ( -
- setSelectedStatus(StatusOptions.ALL_STATUS)} - className="rounded-full" - size="sm" - placeholder={''} - > - X - -
- )} + value={ + selectedStatus + ? { label: selectedStatus, value: selectedStatus } + : undefined + } + onChange={(item) => + setSelectedStatus((item as SelectOption).value as StatusOptions) + } + onClear={() => setSelectedStatus('')} + />
); diff --git a/packages/frontend/src/components/projects/project/overview/Activity/Activity.tsx b/packages/frontend/src/components/projects/project/overview/Activity/Activity.tsx new file mode 100644 index 00000000..252d85a1 --- /dev/null +++ b/packages/frontend/src/components/projects/project/overview/Activity/Activity.tsx @@ -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 ( +
+
+ Activity + +
+
+ {activities.map((activity, index) => { + return ; + })} +
+
+ ); +}; diff --git a/packages/frontend/src/components/projects/project/ActivityCard.tsx b/packages/frontend/src/components/projects/project/overview/Activity/ActivityCard.tsx similarity index 95% rename from packages/frontend/src/components/projects/project/ActivityCard.tsx rename to packages/frontend/src/components/projects/project/overview/Activity/ActivityCard.tsx index 3f3cc55e..6d86ecd5 100644 --- a/packages/frontend/src/components/projects/project/ActivityCard.tsx +++ b/packages/frontend/src/components/projects/project/overview/Activity/ActivityCard.tsx @@ -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) => {
- +
) => { + const styledIcon = cloneElement({ + element: icon, + className: 'w-4 h-4', + }); + + return ( +
+
+ {styledIcon} + {label} +
+
{children}
+
+ ); +}; diff --git a/packages/frontend/src/components/shared/Avatar/Avatar.theme.ts b/packages/frontend/src/components/shared/Avatar/Avatar.theme.ts index b7522cfe..a72a1bc7 100644 --- a/packages/frontend/src/components/shared/Avatar/Avatar.theme.ts +++ b/packages/frontend/src/components/shared/Avatar/Avatar.theme.ts @@ -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', diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.tsx b/packages/frontend/src/components/shared/Calendar/Calendar.tsx index bf01dc06..613f785f 100644 --- a/packages/frontend/src/components/shared/Calendar/Calendar.tsx +++ b/packages/frontend/src/components/shared/Calendar/Calendar.tsx @@ -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 (
{actions ? ( actions ) : ( <> - - + {value && ( + + )} +
+ + +
)}
diff --git a/packages/frontend/src/components/shared/CustomIcon/BranchStrokeIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/BranchStrokeIcon.tsx new file mode 100644 index 00000000..e8b95175 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/BranchStrokeIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const BranchStrokeIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CalendarDaysIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CalendarDaysIcon.tsx new file mode 100644 index 00000000..2528559e --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CalendarDaysIcon.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CalendarDaysIcon = (props: CustomIconProps) => { + return ( + + + + + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx index 6a51210a..7e841bb1 100644 --- a/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx +++ b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx @@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => { return ( ); diff --git a/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx new file mode 100644 index 00000000..79fe29b6 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CheckRadioOutlineIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx index 145e68e1..7811a1ec 100644 --- a/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx +++ b/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx @@ -3,17 +3,11 @@ import { CustomIcon, CustomIconProps } from './CustomIcon'; export const CheckRoundFilledIcon = (props: CustomIconProps) => { return ( - + diff --git a/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx new file mode 100644 index 00000000..deb69392 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CirclePlaceholderOnIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx new file mode 100644 index 00000000..38bb05f4 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CommitIcon.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CommitIcon = (props: CustomIconProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx new file mode 100644 index 00000000..34975bec --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CopyIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CopyIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx new file mode 100644 index 00000000..78f563c5 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CrossCircleIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CursorBoxIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CursorBoxIcon.tsx new file mode 100644 index 00000000..df7889cc --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CursorBoxIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CursorBoxIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/GithubStrokeIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/GithubStrokeIcon.tsx new file mode 100644 index 00000000..74d259ba --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/GithubStrokeIcon.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const GithubStrokeIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/LinkChainIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/LinkChainIcon.tsx new file mode 100644 index 00000000..66ff9cd4 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/LinkChainIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const LinkChainIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/LinkIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/LinkIcon.tsx new file mode 100644 index 00000000..2f0aa923 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/LinkIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const LinkIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx new file mode 100644 index 00000000..720bfc22 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/LoaderIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const LoaderIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx new file mode 100644 index 00000000..7f685677 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/MinusCircleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const MinusCircleIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx new file mode 100644 index 00000000..ca818a25 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const RefreshIcon = (props: CustomIconProps) => { + return ( + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx new file mode 100644 index 00000000..488fba8f --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/RocketIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const RocketIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/StorageIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/StorageIcon.tsx new file mode 100644 index 00000000..38a651f1 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/StorageIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const StorageIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx new file mode 100644 index 00000000..19e40ede --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const TrendingIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx new file mode 100644 index 00000000..713a0b6e --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/UndoIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const UndoIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx new file mode 100644 index 00000000..d5303a60 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const WarningTriangleIcon = (props: CustomIconProps) => { + return ( + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 26a38f55..5b853217 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -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'; diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts index 522bd348..ee1b7466 100644 --- a/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts @@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants'; export const datePickerTheme = tv({ slots: { - input: [], + input: ['w-full'], }, }); diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx index 99fd82a5..464b9b74 100644 --- a/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx @@ -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 ( - + setOpen(true)} />} + leftIcon={ setOpen(true)} />} + rightIcon={} 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} /> diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts index b1ecb705..0144d578 100644 --- a/packages/frontend/src/components/shared/Input/Input.theme.ts +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -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', diff --git a/packages/frontend/src/components/shared/Input/Input.tsx b/packages/frontend/src/components/shared/Input/Input.tsx index fb3fd7d6..8dcc2bed 100644 --- a/packages/frontend/src/components/shared/Input/Input.tsx +++ b/packages/frontend/src/components/shared/Input/Input.tsx @@ -47,15 +47,15 @@ export const Input = ({ helperIcon: helperIconCls, } = inputTheme({ ...styleProps }); - const renderLabels = useMemo( - () => ( -
+ const renderLabels = useMemo(() => { + if (!label && !description) return null; + return ( +

{label}

{description}

- ), - [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 (
{state && cloneIcon(, { @@ -82,17 +83,16 @@ export const Input = ({ })}

{helperText}

- ), - [cloneIcon, state, helperIconCls, helperText, helperTextCls], - ); + ); + }, [cloneIcon, state, helperIconCls, helperText, helperTextCls]); return ( -
+
{renderLabels}
{leftIcon && renderLeftIcon} { + 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 = ({ + tooltipProps, + children, + content, + className, + as, + ...props +}: PropsWithChildren>) => { + 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 ( + + + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/OverflownText/index.ts b/packages/frontend/src/components/shared/OverflownText/index.ts new file mode 100644 index 00000000..1ffb16f2 --- /dev/null +++ b/packages/frontend/src/components/shared/OverflownText/index.ts @@ -0,0 +1 @@ +export * from './OverflownText'; diff --git a/packages/frontend/src/components/shared/Radio/Radio.theme.ts b/packages/frontend/src/components/shared/Radio/Radio.theme.ts index 0b84601e..84d8fd01 100644 --- a/packages/frontend/src/components/shared/Radio/Radio.theme.ts +++ b/packages/frontend/src/components/shared/Radio/Radio.theme.ts @@ -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', }, }); diff --git a/packages/frontend/src/components/shared/Radio/Radio.tsx b/packages/frontend/src/components/shared/Radio/Radio.tsx index 96542493..80468001 100644 --- a/packages/frontend/src/components/shared/Radio/Radio.tsx +++ b/packages/frontend/src/components/shared/Radio/Radio.tsx @@ -49,14 +49,15 @@ export const Radio = ({ className, options, orientation, + variant, ...props }: RadioProps) => { - const { root } = radioTheme({ orientation }); + const { root } = radioTheme({ orientation, variant }); return ( {options.map((option) => ( - + ))} ); diff --git a/packages/frontend/src/components/shared/Radio/RadioItem.tsx b/packages/frontend/src/components/shared/Radio/RadioItem.tsx index 177af9db..d77f4752 100644 --- a/packages/frontend/src/components/shared/Radio/RadioItem.tsx +++ b/packages/frontend/src/components/shared/Radio/RadioItem.tsx @@ -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 { /** * 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 ( -
+
+ ); }; diff --git a/packages/frontend/src/components/shared/Select/Select.theme.ts b/packages/frontend/src/components/shared/Select/Select.theme.ts index 43d4b0f4..56b54f24 100644 --- a/packages/frontend/src/components/shared/Select/Select.theme.ts +++ b/packages/frontend/src/components/shared/Select/Select.theme.ts @@ -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'], diff --git a/packages/frontend/src/components/shared/Select/Select.tsx b/packages/frontend/src/components/shared/Select/Select.tsx index 963cc0bb..2f1667de 100644 --- a/packages/frontend/src/components/shared/Select/Select.tsx +++ b/packages/frontend/src/components/shared/Select/Select.tsx @@ -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(null); + const [selectedItem, setSelectedItem] = useState( + (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({ + 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) => { e.stopPropagation(); @@ -284,29 +267,32 @@ export const Select = ({ onClear?.(); }; - const renderLabels = useMemo( - () => ( -
+ const renderLabels = useMemo(() => { + if (!label && !description) return null; + return ( +

{label}

{description}

- ), - [theme, label, description], - ); + ); + }, [theme, label, description]); const renderLeftIcon = useMemo(() => { return (
- {cloneIcon(leftIcon, { className: theme.icon(), 'aria-hidden': true })} + {cloneIcon(selectedItem?.leftIcon ? selectedItem.leftIcon : leftIcon, { + className: theme.icon(), + 'aria-hidden': true, + })}
); - }, [cloneIcon, theme, leftIcon]); + }, [cloneIcon, theme, leftIcon, selectedItem]); const renderRightIcon = useMemo(() => { return (
{clearable && (selectedItems.length > 0 || selectedItem) && ( - @@ -314,14 +300,15 @@ export const Select = ({ {rightIcon ? ( cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true }) ) : ( - + )}
); - }, [cloneIcon, theme, rightIcon]); + }, [cloneIcon, theme, rightIcon, selectedItem, selectedItems, clearable]); - const renderHelperText = useMemo( - () => ( + const renderHelperText = useMemo(() => { + if (!helperText) return null; + return (
{error && cloneIcon(, { @@ -329,13 +316,13 @@ export const Select = ({ })}

{helperText}

- ), - [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, }, )} /> diff --git a/packages/frontend/src/components/shared/Steps/Step/Step.theme.ts b/packages/frontend/src/components/shared/Steps/Step/Step.theme.ts new file mode 100644 index 00000000..8e2b7a53 --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/Step/Step.theme.ts @@ -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; diff --git a/packages/frontend/src/components/shared/Steps/Step/Step.tsx b/packages/frontend/src/components/shared/Steps/Step/Step.tsx new file mode 100644 index 00000000..9d731139 --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/Step/Step.tsx @@ -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 ( +
+ {orientation === 'horizontal' && } +
+ ); + }, + [orientation, theme], + ); + + return ( + <> + {renderConnector(index)} +
  • + { +
    + {completed ? ( + + ) : ( + index + )} +
    + } +

    {label}

    +
  • + + ); +}; diff --git a/packages/frontend/src/components/shared/Steps/Step/index.ts b/packages/frontend/src/components/shared/Steps/Step/index.ts new file mode 100644 index 00000000..84d83fcb --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/Step/index.ts @@ -0,0 +1,2 @@ +export * from './Step'; +export * from './Step.theme'; diff --git a/packages/frontend/src/components/shared/Steps/Steps.theme.ts b/packages/frontend/src/components/shared/Steps/Steps.theme.ts new file mode 100644 index 00000000..4c3b2acb --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/Steps.theme.ts @@ -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; diff --git a/packages/frontend/src/components/shared/Steps/Steps.tsx b/packages/frontend/src/components/shared/Steps/Steps.tsx new file mode 100644 index 00000000..cac61ff9 --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/Steps.tsx @@ -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 { + /** + * The index of the current step + */ + currentIndex: number; + /** + * The steps to render + */ + steps: Pick[]; +} + +export const Steps = ({ + currentIndex, + steps = [], + className, + orientation, + ...props +}: StepsProps) => { + const theme = stepsTheme(); + + return ( +
      + {steps.map((step, i) => ( + + + + ))} +
    + ); +}; diff --git a/packages/frontend/src/components/shared/Steps/index.ts b/packages/frontend/src/components/shared/Steps/index.ts new file mode 100644 index 00000000..27d56a3b --- /dev/null +++ b/packages/frontend/src/components/shared/Steps/index.ts @@ -0,0 +1,2 @@ +export * from './Steps'; +export * from './Steps.theme'; diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts index 3f9ef417..783d242b 100644 --- a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts +++ b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts @@ -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 diff --git a/packages/frontend/src/components/shared/Tag/Tag.theme.ts b/packages/frontend/src/components/shared/Tag/Tag.theme.ts index 086b59b3..884f79bc 100644 --- a/packages/frontend/src/components/shared/Tag/Tag.theme.ts +++ b/packages/frontend/src/components/shared/Tag/Tag.theme.ts @@ -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'], }, }, }, diff --git a/packages/frontend/src/components/shared/Tag/Tag.tsx b/packages/frontend/src/components/shared/Tag/Tag.tsx index 086d3515..8be34c85 100644 --- a/packages/frontend/src/components/shared/Tag/Tag.tsx +++ b/packages/frontend/src/components/shared/Tag/Tag.tsx @@ -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 ( -
    +
    {renderLeftIcon}

    {children}

    {renderRightIcon} diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index c471ab5d..5805ba37 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -32,7 +32,7 @@ const ProjectSearch = () => { }, []); return ( -
    +
    @@ -64,7 +64,7 @@ const ProjectSearch = () => {
    -
    +
    diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index aab2d652..feee4595 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -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 = () => {
    + {/* Steps */} +
    +
    +

    Steps

    +
    + {renderVerticalSteps()} + {renderHorizontalSteps()} +
    +
    +
    + +
    + {/* Tag */}
    diff --git a/packages/frontend/src/pages/components/renders/steps.tsx b/packages/frontend/src/pages/components/renders/steps.tsx new file mode 100644 index 00000000..2d3f3da5 --- /dev/null +++ b/packages/frontend/src/pages/components/renders/steps.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Steps } from 'components/shared/Steps'; + +export const renderVerticalSteps = () => { + return ( + + ); +}; + +export const renderHorizontalSteps = () => { + return ( + + ); +}; diff --git a/packages/frontend/src/pages/org-slug/projects/Id.tsx b/packages/frontend/src/pages/org-slug/projects/Id.tsx index 37ce9719..cf333ba3 100644 --- a/packages/frontend/src/pages/org-slug/projects/Id.tsx +++ b/packages/frontend/src/pages/org-slug/projects/Id.tsx @@ -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(null); const [repoUrl, setRepoUrl] = useState(''); @@ -69,96 +68,65 @@ const Id = () => {
    {project ? ( <> -
    - - - {project?.name} - - +
    +
    +
    + + + + - - +
    - -
    - - - - - Overview - - - - - Deployments - - - - - Database - - - - - Integrations - - - - - Settings - - - - + +
    + + + + Overview + + + Deployments + + + Integrations + + + Settings + + + {/* Not wrapping in Tab.Content because we are using Outlet */} +
    - +
    ) : ( -

    Project not found

    +
    + + Project not found. + +
    )}
    ); diff --git a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx index d819a6f1..3d61792d 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/Template.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/Template.tsx @@ -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 (
    -
    - -
    {template?.name}
    +
    - +
    diff --git a/packages/frontend/src/pages/org-slug/projects/create/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/index.tsx index 1c86ee3b..49eeed96 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/index.tsx @@ -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 ? ( <>
    - - Start with template + + Start with a template
    {templates.map((template) => { @@ -28,7 +28,7 @@ const NewProject = () => { })}
    - + Import a repository diff --git a/packages/frontend/src/pages/org-slug/projects/create/layout.tsx b/packages/frontend/src/pages/org-slug/projects/create/layout.tsx index 3dad337a..906a6215 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/layout.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/layout.tsx @@ -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(); + const closeBtnLink = `/${orgSlug}`; + + const heading = ( + + Create new project + + ); + return ( -
    -
    -
    - - Create new project - - - - + <> + {/* Desktop */} +
    -
    - + +
    + +
    -
    + + {/* Mobile */} + {/* Setting modal={false} so even if modal is active on desktop, it doesn't block clicks */} + + + {/* Not using since modal={false} disables it and its content will not show */} +
    + + {/* Heading */} +
    + {heading} + + +
    + + {/* Border */} + + + {/* Page content */} +
    + +
    +
    +
    +
    +
    + ); }; diff --git a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx index 9ff50c84..9fc8795e 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx @@ -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({ + const { handleSubmit, control, reset } = useForm({ defaultValues: { framework: 'React', repoName: '', @@ -110,86 +113,67 @@ const CreateRepo = () => { return (
    -
    - - Create a repository - - - The project will be cloned into this repository - -
    -
    -
    Framework
    -
    - - -
    -
    -
    -
    Git account
    +
    + + Create a repository + + + The project will be cloned into this repository + +
    +
    + Git account ( - - {gitAccounts.map((account, key) => ( - - ))} - + render={({ field: { value, onChange } }) => ( + + Name the repo + ( + + )} />
    -
    -
    - -
    -
    - +
    +
    + +
    ); diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index d26a2dd4..1437754f 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -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 ( -
    +
    setFilterValue(value)} /> -
    +
    {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} /> ); }) ) : ( -
    -
    - + // TODO: Update the height based on the layout, need to re-styling the layout similar to create project layout +
    +
    +

    No deployments found - - - Please change your search query or filters - - +

    +

    + Please change your search query or filters. +

    +
    )}
    -
    +
    ); }; diff --git a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx index 21f281c1..c1337045 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx @@ -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 ( -
    -
    -
    +
    +
    +
    -
    - {project.name} - +
    + + {project.name} + +

    {project.subDomain} - +

    -
    -
    ^ Domain
    + }> {liveDomain ? ( - + }> + Connected + ) : ( -
    - +
    + }> + Not connected +
    )} -
    +
    {project.deployments.length !== 0 ? ( <> -
    -

    ^ Source

    -

    ^ {project.deployments[0]?.branch}

    -
    -
    -

    ^ Deployment

    -

    {liveDomain?.name}

    -
    -
    -

    ^ Created

    -

    - {relativeTimeMs(project.deployments[0].createdAt)} by ^{' '} - - {formatAddress(project.deployments[0].createdBy.name ?? '')} - -

    -
    + {/* SOURCE */} + }> +
    + + + {project.deployments[0]?.branch} + +
    +
    + + {/* DATABASE */} + }> +
    + + + {/* // TODO: add db name + dbname + */} + + + +
    +
    + + {/* DEPLOYMENT */} + }> +
    + + + {liveDomain?.name}{' '} + + + +
    +
    + + {/* DEPLOYMENT DATE */} + }> +
    + {relativeTimeMs(project.deployments[0].createdAt)} + by + + {project.deployments[0]?.createdBy?.name} +
    +
    ) : ( -
    No current deployment found
    +

    + No current deployment found. +

    )}
    -
    -
    - Activity - -
    -
    - {activities.map((activity, index) => { - return ( - - ); - })} -
    -
    +
    ); }; diff --git a/packages/frontend/src/utils/cloneElement.tsx b/packages/frontend/src/utils/cloneElement.tsx new file mode 100644 index 00000000..202f9e45 --- /dev/null +++ b/packages/frontend/src/utils/cloneElement.tsx @@ -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 { + 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) + ?.className; + + return reactCloneElement(child as ReactElement, { + className: themeStyle + ? themeStyle({ + className: cn(originalClassName, className), // overriding icon classNames + }) + : originalClassName, + ...props, + }); + })} + + ); + } + return <>; +}; diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index c362e898..b5fa02b0 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -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', diff --git a/yarn.lock b/yarn.lock index 60ab1754..2708695e 100644 --- a/yarn.lock +++ b/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==