diff --git a/components/cloudProviderCard/cloudProviderCard.styled.ts b/components/cloudProviderCard/cloudProviderCard.styled.ts index 6c0fad8b..840b878d 100644 --- a/components/cloudProviderCard/cloudProviderCard.styled.ts +++ b/components/cloudProviderCard/cloudProviderCard.styled.ts @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import NextLink from 'next/link'; import Card from '../card'; @@ -24,11 +25,16 @@ export const DetailsContainer = styled.div` `; export const LabelContainer = styled.div` + color: ${({ theme }) => theme.colors.volcanicSand}; display: flex; gap: 16px; `; -export const LinkContent = styled.a` +export const Link = styled(NextLink)` color: ${({ theme }) => theme.colors.primary}; text-decoration: none; + + &:hover { + text-decoration: underline; + } `; diff --git a/components/cloudProviderCard/index.tsx b/components/cloudProviderCard/index.tsx index 0aed6063..18d6eeea 100644 --- a/components/cloudProviderCard/index.tsx +++ b/components/cloudProviderCard/index.tsx @@ -1,6 +1,5 @@ import React, { FunctionComponent } from 'react'; import Image from 'next/image'; -import Link from 'next/link'; import { CardProps } from '../card'; import Typography from '../typography'; @@ -12,12 +11,7 @@ import digitalOceanLogo from '../../assets/digital_ocean_logo.svg'; import vultrLogo from '../../assets/vultr_logo.svg'; import Tag from '../tag'; -import { - CardContainer, - DetailsContainer, - LinkContent, - LabelContainer, -} from './cloudProviderCard.styled'; +import { CardContainer, DetailsContainer, Link, LabelContainer } from './cloudProviderCard.styled'; const PROVIDER_OPTIONS: Record< InstallationType, @@ -60,7 +54,9 @@ const PROVIDER_OPTIONS: Record< logoSrc: digitalOceanLogo, label: 'Digital Ocean', description: - 'A powerful open source cloud native tool set for identity and infrastructure management, application delivery, and secrets managament.', + 'A cloud platform that allows developers to build, deploy, and scale applications. Its simplicity, ease of use and affordable pricing make it a popular choice for small businesses and startups. ', + learnMoreLink: '#', + height: 50, width: 50, beta: true, @@ -69,7 +65,7 @@ const PROVIDER_OPTIONS: Record< logoSrc: vultrLogo, label: 'Vultr', description: - 'A powerful open source cloud native tool set for identity and infrastructure management, application delivery, and secrets managament.', + 'A cloud hosting provider that offers high-performance SSD-based cloud servers, block storage, object storage, and dedicated servers in multiple locations worldwide. ', learnMoreLink: '#', height: 43, width: 50, @@ -100,8 +96,9 @@ const CloudProviderCard: FunctionComponent = ({ {description} {learnMoreLink && ( - - Learn More + + {' '} + Learn More )} diff --git a/components/errorBanner/index.tsx b/components/errorBanner/index.tsx index e3f74b21..be759822 100644 --- a/components/errorBanner/index.tsx +++ b/components/errorBanner/index.tsx @@ -11,7 +11,7 @@ export interface ErrorBannerProps { text: string; } -const ErrorBanner: FunctionComponent = ({ details, text }) => { +const ErrorBanner: FunctionComponent = ({ text }) => { return (
diff --git a/components/installationButtons/index.tsx b/components/installationButtons/index.tsx index 644b21ee..49e3ded6 100644 --- a/components/installationButtons/index.tsx +++ b/components/installationButtons/index.tsx @@ -24,7 +24,7 @@ const InstallationButtons: FunctionComponent = ({ }) => ( {showBackButton && ( - )} diff --git a/components/installationStepContainer/index.tsx b/components/installationStepContainer/index.tsx index cbc00453..fc375e8d 100644 --- a/components/installationStepContainer/index.tsx +++ b/components/installationStepContainer/index.tsx @@ -37,13 +37,21 @@ const InstallationStepContainer: FunctionComponent { - const { completedSteps } = useAppSelector(({ cluster }) => ({ + const { completedSteps, isProvisioned } = useAppSelector(({ cluster }) => ({ ...cluster, })); - const progress = useMemo( - () => Math.round((completedSteps.length / Object.keys(CLUSTER_CHECKS).length) * 100), - [completedSteps.length], - ); + const progress = useMemo(() => { + const clusterChecks = Object.keys(CLUSTER_CHECKS); + const progress = Math.round((completedSteps.length / clusterChecks.length) * 100); + + if (completedSteps.length === clusterChecks.length && !isProvisioned) { + return 98; + } else if (isProvisioned) { + return 100; + } + + return progress; + }, [completedSteps.length, isProvisioned]); return ( diff --git a/components/installationStepContainer/installationStepContainer.styled.ts b/components/installationStepContainer/installationStepContainer.styled.ts index 46d364e4..0474211a 100644 --- a/components/installationStepContainer/installationStepContainer.styled.ts +++ b/components/installationStepContainer/installationStepContainer.styled.ts @@ -36,6 +36,7 @@ export const Content = styled(Column)<{ hasInfo?: boolean; isProvisionStep: bool `; export const InstallTitle = styled(Typography)` + color: ${({ theme }) => theme.colors.volcanicSand}; margin: 40px 0 24px 0 !important; text-align: center; `; diff --git a/components/linearProgress/index.tsx b/components/linearProgress/index.tsx index 63942d5e..83cdc173 100644 --- a/components/linearProgress/index.tsx +++ b/components/linearProgress/index.tsx @@ -3,11 +3,12 @@ import LinearProgressMui, { LinearProgressProps } from '@mui/material/LinearProg import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -import { LIGHT_GREY, PRIMARY, VOLCANIC_SAND } from '../../constants/colors'; +import { ASMANI_SKY, LIGHT_GREY, PRIMARY, VOLCANIC_SAND } from '../../constants/colors'; import { Container } from './linearProgress.styled'; const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { + const isCompleted = props.value === 100; return ( - Provisioning cluster: + {isCompleted ? 'Completed!' : 'Provisioning cluster:'} {`${Math.round( props.value, @@ -35,7 +36,9 @@ const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) 'background': LIGHT_GREY, 'height': '9px', '> span': { - background: `linear-gradient(270deg, ${PRIMARY} 0%, #81E2B4 100%)`, + background: isCompleted + ? ASMANI_SKY + : `linear-gradient(270deg, ${PRIMARY} 0%, ${ASMANI_SKY} 100%)`, borderRadius: '55px', }, }} diff --git a/components/progress/progress.styled.tsx b/components/progress/progress.styled.tsx index 6bccd673..7871e930 100644 --- a/components/progress/progress.styled.tsx +++ b/components/progress/progress.styled.tsx @@ -8,14 +8,14 @@ import { import styled from 'styled-components'; import { theme } from '../../theme'; -import { BISCAY, PORT_GORE, SALTBOX_BLUE } from '../../constants/colors'; +import { BISCAY, PORT_GORE } from '../../constants/colors'; export const Label = muiStyled(StepLabel)(() => ({ - [`& .${stepLabelClasses.label}`]: { - color: `${SALTBOX_BLUE}`, + [`& .${stepLabelClasses.completed}`]: { + color: theme.colors.saltboxBlue, }, [`& .${stepLabelClasses.active}`]: { - color: `${BISCAY}`, + color: BISCAY, }, [`& .${stepLabelClasses.disabled}`]: { color: theme.colors.saltboxBlue, diff --git a/constants/cluster.tsx b/constants/cluster.tsx index 50cba67b..2ef995bf 100644 --- a/constants/cluster.tsx +++ b/constants/cluster.tsx @@ -7,21 +7,19 @@ export const CLUSTER_MENU_OPTIONS = [ { label: DELETE_OPTION, color: FIRE_BRICK }, ]; -export const CLUSTER_CHECKS: { [key: string]: string } = { - install_tools_check: 'Installing tools', - domain_liveness_check: 'Domain liveness check', - git_init_check: 'Initializing git', - kbot_setup_check: 'Kbot setup', - gitops_ready_check: 'Initializing gitops', - git_terraform_apply_check: 'Git terraform apply', - gitops_pushed_check: 'Gitops repos pushed', - cloud_terraform_apply_check: 'Cloud terraform apply', - cluster_secrets_created_check: 'Cretating cluster secrets', - argocd_install_check: 'Installing argocd', - argocd_initialize_check: 'Initializing argocd', - argocd_create_registry_check: 'Create argocd registry', - argocd_delete_registry_check: 'Delete argocd registry', - vault_initialized_check: 'Initializing vault', - vault_terraform_apply_check: 'Vault terraform apply', - users_terraform_apply_check: 'Users terraform apply', +export const CLUSTER_CHECKS: { [key: string]: { label: string; order: number } } = { + install_tools_check: { label: 'Installing tools', order: 1 }, + domain_liveness_check: { label: 'Domain liveness check', order: 2 }, + kbot_setup_check: { label: 'Kbot setup', order: 3 }, + git_init_check: { label: 'Initializing git', order: 4 }, + gitops_ready_check: { label: 'Initializing gitops', order: 5 }, + git_terraform_apply_check: { label: 'Git terraform apply', order: 6 }, + gitops_pushed_check: { label: 'Gitops repos pushed', order: 7 }, + cloud_terraform_apply_check: { label: 'Cloud terraform apply', order: 8 }, + cluster_secrets_created_check: { label: 'Creating cluster secrets', order: 9 }, + argocd_install_check: { label: 'Installing argocd', order: 10 }, + argocd_initialize_check: { label: 'Initializing argocd', order: 11 }, + vault_initialized_check: { label: 'Initializing vault', order: 12 }, + vault_terraform_apply_check: { label: 'Vault terraform apply', order: 13 }, + users_terraform_apply_check: { label: 'Users terraform apply', order: 14 }, }; diff --git a/constants/colors.ts b/constants/colors.ts index 28ceb4f4..8cae5a72 100644 --- a/constants/colors.ts +++ b/constants/colors.ts @@ -48,3 +48,4 @@ export const LIBERTY_BLUE = '#0f172a'; export const EXCLUSIVE_PLUM = '#71717A'; export const LAUGHING_ORANGE = '#F59E0B'; export const SEFID_WHITE = '#FEF2F2'; +export const ASMANI_SKY = '#81E2B4'; diff --git a/containers/conciseLogs/conciseLogs.styled.ts b/containers/conciseLogs/conciseLogs.styled.ts index 708097a9..4db28e99 100644 --- a/containers/conciseLogs/conciseLogs.styled.ts +++ b/containers/conciseLogs/conciseLogs.styled.ts @@ -8,6 +8,10 @@ export const Container = styled.div` overflow: auto; `; +export const EstimatedTime = styled.div` + margin-bottom: 16px; +`; + export const Step = styled.div` align-items: center; display: flex; @@ -23,9 +27,10 @@ export const StepNumber = styled(Typography)` `; export const StepLabel = styled(Typography)` - font-style: normal; - font-weight: 400; + font-family: 'Roboto Mono'; font-size: 14px; + font-style: normal; + font-weight: 700; line-height: 18px; `; diff --git a/containers/conciseLogs/index.tsx b/containers/conciseLogs/index.tsx index 287d97b7..623f8c79 100644 --- a/containers/conciseLogs/index.tsx +++ b/containers/conciseLogs/index.tsx @@ -1,47 +1,81 @@ import React, { FunctionComponent, useMemo } from 'react'; +import sortBy from 'lodash/sortBy'; import { CLUSTER_CHECKS } from '../../constants/cluster'; import { useAppSelector } from '../../redux/store'; +import { InstallationType } from '../../types/redux'; -import { Container, Step, StepLabel, StepNumber, Success, SuccessText } from './conciseLogs.styled'; +import { + Container, + EstimatedTime, + Step, + StepLabel, + StepNumber, + Success, + SuccessText, +} from './conciseLogs.styled'; +const ESTIMATED_TIMES_BY_CLOUD: Record = { + [InstallationType.LOCAL]: 5, + [InstallationType.AWS]: 35, + [InstallationType.CIVO]: 10, + [InstallationType.DIGITAL_OCEAN]: 7, + [InstallationType.VULTR]: 10, +}; export interface ConciseLogsProps { - completedSteps: Array; + completedSteps: Array<{ label: string; order: number }>; } const ConciseLogs: FunctionComponent = ({ completedSteps }) => { - const { isError, isProvisioned, lastErrorCondition } = useAppSelector(({ cluster }) => ({ - cluster: cluster.selectedCluster, - isProvisioned: cluster.isProvisioned, - lastErrorCondition: cluster.lastErrorCondition, - isError: cluster.isError, - })); + const { installType, isError, isProvisioned, lastErrorCondition } = useAppSelector( + ({ cluster, installation }) => ({ + cluster: cluster.selectedCluster, + isProvisioned: cluster.isProvisioned, + lastErrorCondition: cluster.lastErrorCondition, + isError: cluster.isError, + installType: installation.installType, + }), + ); const lastStep = useMemo(() => { if (completedSteps.length < Object.keys(CLUSTER_CHECKS).length) { - const nextStep = Object.values(CLUSTER_CHECKS)[completedSteps.length + 1]; + const nextStep = Object.values(CLUSTER_CHECKS)[completedSteps.length]; return nextStep; } return null; }, [completedSteps.length]); + const estimatedTime = useMemo( + () => ESTIMATED_TIMES_BY_CLOUD[installType as InstallationType], + [installType], + ); + return ( - {completedSteps.map((step, index) => ( + + ⏰ Estimated time: {estimatedTime} minutes + + {sortBy(completedSteps, 'order').map((step, index) => ( <>✅ - {`[${index + 1}/${Object.keys(CLUSTER_CHECKS).length - 1}]`} - {step}{' '} + {`[${index + 1}/${Object.keys(CLUSTER_CHECKS).length}]`} + {step.label}{' '} ))} {!isError && lastStep && ( <>💫 {`[${completedSteps.length + 1}/${ - Object.keys(CLUSTER_CHECKS).length - 1 + Object.keys(CLUSTER_CHECKS).length }]`} - {lastStep}{' '} + {lastStep.label}{' '} + + )} + {!isProvisioned && !lastStep && !isError && ( + + <>💫 + Wrapping up{' '} )} {isError && ( diff --git a/containers/installationsSelection/index.tsx b/containers/installationsSelection/index.tsx index 2651b7d2..639c06c1 100644 --- a/containers/installationsSelection/index.tsx +++ b/containers/installationsSelection/index.tsx @@ -65,7 +65,7 @@ export const InstallationsSelection: FunctionComponent {gitProvider && ( - Now Select your cloud adventure + Now select your cloud adventure {INSTALLATION_TYPES.map((type) => ( theme.colors.volcanicSand}; margin: 40px 0 24px 0 !important; `; diff --git a/containers/provision/index.tsx b/containers/provision/index.tsx index 89a61ca6..a6aee970 100644 --- a/containers/provision/index.tsx +++ b/containers/provision/index.tsx @@ -19,8 +19,9 @@ import { setConfigValues } from '../../redux/slices/config.slice'; import { clearClusterState } from '../../redux/slices/cluster.slice'; import AdvancedOptions from '../clusterForms/shared/advancedOptions'; import ErrorBanner from '../../components/errorBanner'; +import Button from '../../components/button'; -import { AdvancedOptionsContainer, Form, FormContent } from './provision.styled'; +import { AdvancedOptionsContainer, ErrorContainer, Form, FormContent } from './provision.styled'; export interface ProvisionProps { apiUrl: string; @@ -33,6 +34,8 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) ({ installation }) => installation, ); + const { isProvisioned } = useAppSelector(({ cluster }) => cluster); + const { stepTitles, formFlow: FormFlow, @@ -71,9 +74,19 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) const isValid = useMemo(() => { if (installationStep === 0) { return !!gitProvider && !!installType; + } else if (isProvisionStep) { + return isProvisioned; } return isFormValid && !error; - }, [error, gitProvider, installType, installationStep, isFormValid]); + }, [ + error, + gitProvider, + installType, + installationStep, + isFormValid, + isProvisionStep, + isProvisioned, + ]); const handleNextButtonClick = useCallback(async () => { dispatch(setInstallationStep(installationStep + 1)); @@ -94,7 +107,7 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) if (isSetupStep) { try { - await dispatch(createCluster({ apiUrl, values })).unwrap(); + await provisionCluster(); handleNextButtonClick(); } catch (error) { //todo: error handling to be defined @@ -105,6 +118,12 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) } }; + const provisionCluster = useCallback(async () => { + await dispatch(clearError()); + await dispatch(clearClusterState()); + await dispatch(createCluster({ apiUrl })).unwrap(); + }, [apiUrl, dispatch]); + const form = useMemo(() => { if (installationStep === 0) { return ; @@ -113,7 +132,16 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) return ( <> - {error && } + {error && ( + + + {isProvisionStep && ( + + )} + + )} = ({ apiUrl, useTelemetry }) domainName={values?.domainName as string} /> - {isSetupStep && ( + {isSetupStep && installType !== InstallationType.LOCAL && ( = ({ apiUrl, useTelemetry }) ); }, [ - FormFlow, - control, - error, - hasInfo, installationStep, + hasInfo, isLastStep, - isSetupStep, isProvisionStep, - reset, + error, + provisionCluster, + FormFlow, + control, setValue, - stepTitles, trigger, + watch, values?.clusterName, values?.domainName, - watch, + isSetupStep, + installType, + stepTitles, + reset, ]); useEffect(() => { @@ -171,7 +201,7 @@ const Provision: FunctionComponent = ({ apiUrl, useTelemetry }) activeStep={installationStep} steps={stepTitles} installationTitle={installTitle} - showBackButton={installationStep > 0} + showBackButton={installationStep > 0 && !isProvisionStep} onNextButtonClick={handleNextButtonClick} onBackButtonClick={handleBackButtonClick} nextButtonText={isSetupStep ? 'Create cluster' : 'Next'} diff --git a/containers/provision/provision.styled.ts b/containers/provision/provision.styled.ts index 6ab8b8e6..b9a915f2 100644 --- a/containers/provision/provision.styled.ts +++ b/containers/provision/provision.styled.ts @@ -14,6 +14,12 @@ export const Form = styled(Box)` height: 100%; `; +export const ErrorContainer = styled.div` + align-items: center; + display: flex; + gap: 24px; +`; + export const FormContent = styled(FormContainer)<{ hasInfo: boolean; isLastStep: boolean; diff --git a/containers/terminalLogs/terminalLogs.styled.ts b/containers/terminalLogs/terminalLogs.styled.ts index 67259848..8355c635 100644 --- a/containers/terminalLogs/terminalLogs.styled.ts +++ b/containers/terminalLogs/terminalLogs.styled.ts @@ -11,7 +11,7 @@ export const Container = styled.div` padding: 0 16px; position: relative; height: 551px; - width: 988px; + width: calc(100% - 30px); `; export const Search = styled.div` diff --git a/containers/terminalLogs/terminalLogs.tsx b/containers/terminalLogs/terminalLogs.tsx index 43ec7c12..8f3a8143 100644 --- a/containers/terminalLogs/terminalLogs.tsx +++ b/containers/terminalLogs/terminalLogs.tsx @@ -33,7 +33,6 @@ import { Close, Container, Search, TerminalView } from './terminalLogs.styled'; import 'xterm/css/xterm.css'; -const DATE_REGEX = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})/; const SEARCH_OPTIONS = { caseSensitive: false }; enum TERMINAL_TABS { @@ -148,7 +147,14 @@ const TerminalLogs: FunctionComponent = () => { const emitter = createLogStream(`${apiUrl}/stream`); emitter.on('log', (log) => { - terminal.write(`${log.message.replace(DATE_REGEX, '\x1b[0;37m$1\x1B[0m')}\n`); + const [, time] = log.message.match(/time="([^"]*)"/); + const [, level] = log.message.match(/level=([^"]*)/); + const [, msg] = log.message.match(/msg="([^"]*)"/); + + const logLevel = level.replace(' msg=', '').toUpperCase(); + const logStyle = logLevel.includes('ERROR') ? '\x1b[1;31m' : '\x1b[0;34m'; + + terminal.write(`\x1b[0;37m${time} ${logStyle}${logLevel}:\x1b[1;37m ${msg} \n`); }); emitter.on('error', () => { @@ -168,9 +174,9 @@ const TerminalLogs: FunctionComponent = () => { Object.keys(CLUSTER_CHECKS).forEach((checkKey) => { const step = CLUSTER_CHECKS[checkKey as string]; const isStepCompleted = selectedCluster?.checks[checkKey]; - const isStepAdded = completedSteps.includes(step); + const isStepAdded = completedSteps.find(({ label }) => label === step.label); - if (isStepCompleted && !isStepAdded) { + if (isStepCompleted && !isStepAdded?.label) { dispatch(setCompletedSteps([...completedSteps, step])); } }); diff --git a/redux/slices/cluster.slice.ts b/redux/slices/cluster.slice.ts index 04e7af2a..a287a807 100644 --- a/redux/slices/cluster.slice.ts +++ b/redux/slices/cluster.slice.ts @@ -14,7 +14,7 @@ export interface ApiState { lastErrorCondition?: string; clusters: Array; selectedCluster?: Cluster; - completedSteps: Array; + completedSteps: Array<{ label: string; order: number }>; } export const initialState: ApiState = { diff --git a/redux/thunks/cluster.thunk.ts b/redux/thunks/cluster.thunk.ts index 875338e8..adb6c65a 100644 --- a/redux/thunks/cluster.thunk.ts +++ b/redux/thunks/cluster.thunk.ts @@ -2,7 +2,6 @@ import axios from 'axios'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { AppDispatch, RootState } from '../store'; -import { InstallValues } from '../../types/redux'; import { Cluster, ClusterRequestProps, ClusterResponse } from '../../types/provision'; const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({ @@ -43,40 +42,40 @@ const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({ export const createCluster = createAsyncThunk< Cluster, - { apiUrl: string; values: InstallValues }, + { apiUrl: string }, { dispatch: AppDispatch; state: RootState; } ->('cluster/provisioning', async ({ apiUrl, values }, { getState }) => { +>('cluster/provisioning', async ({ apiUrl }, { getState }) => { const { - installation: { installType, gitProvider }, + installation: { installType, gitProvider, values }, } = getState(); const params = { - clusterName: values.clusterName, - admin_email: values.alertsEmail, + clusterName: values?.clusterName, + admin_email: values?.alertsEmail, cloud_provider: installType?.toString(), - cloud_region: values.cloudRegion, - domain_name: values.domainName, + cloud_region: values?.cloudRegion, + domain_name: values?.domainName, git_owner: values?.gitOwner, git_provider: gitProvider, git_token: values?.gitToken, type: 'mgmt', aws_auth: { - ...values.aws_auth, + ...values?.aws_auth, }, civo_auth: { - ...values.civo_auth, + ...values?.civo_auth, }, digitalocean_auth: { - ...values.digitalocean_auth, + ...values?.digitalocean_auth, }, vultr_auth: { - ...values.vultr_auth, + ...values?.vultr_auth, }, }; - const res = await axios.post(`${apiUrl}/cluster/${values.clusterName || 'kubefirst'}`, params); + const res = await axios.post(`${apiUrl}/cluster/${values?.clusterName || 'kubefirst'}`, params); if ('error' in res) { throw res.error;