diff --git a/package-lock.json b/package-lock.json index 1e1b2c9b..32e269fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "front-aleph-cloud", - "version": "0.2.6", + "version": "0.2.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "front-aleph-cloud", - "version": "0.2.6", + "version": "0.2.7", "dependencies": { - "@aleph-front/aleph-core": "^1.3.4", + "@aleph-front/aleph-core": "^1.3.13", "@fortawesome/fontawesome-svg-core": "^6.3.0", + "@hookform/resolvers": "^3.1.1", "@types/node": "18.14.1", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", @@ -21,11 +22,13 @@ "next": "^13.3.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.45.2", "react-loader-spinner": "^5.3.4", - "styled-components": "^5.3.6", + "styled-components": "^5.3.10", "typescript": "4.9.5", "usehooks-ts": "^2.9.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "devDependencies": { "@babel/plugin-syntax-typescript": "^7.21.4", @@ -131,22 +134,23 @@ } }, "node_modules/@aleph-front/aleph-core": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@aleph-front/aleph-core/-/aleph-core-1.3.4.tgz", - "integrity": "sha512-xyKtffq54GcXO/A0QEgx/Z/NqBosH/uZBl0D4/T7WYQRLJn13bLMc83noeFuu9cjxsQH9blPysZlcczFuh3sxw==", + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@aleph-front/aleph-core/-/aleph-core-1.3.13.tgz", + "integrity": "sha512-s3kjq4OAiVKRlS5ZlfNDlpBsdAo6D2aqA/ZMKmLPb7LHFI56iJOEYPKQiSk72AFzn2a2UBnahs9toLy+Ggv5aw==", "dependencies": { "@monaco-editor/react": "^4.4.6", "react-transition-group": "^4.4.5" }, "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "^6.3.0", - "@fortawesome/free-brands-svg-icons": "^6.3.0", - "@fortawesome/pro-regular-svg-icons": "^6.3.0", - "@fortawesome/react-fontawesome": "^0.1.18", - "@fortawesome/sharp-solid-svg-icons": "^6.3.0", - "react": "^18.1.0", - "styled-components": "^5.3.10", - "twin.macro": "^3.3.1" + "@fortawesome/fontawesome-svg-core": "^6.x", + "@fortawesome/free-brands-svg-icons": "^6.x", + "@fortawesome/pro-regular-svg-icons": "^6.x", + "@fortawesome/react-fontawesome": "^0.1.x", + "@fortawesome/sharp-solid-svg-icons": "^6.x", + "react": "^18.x", + "react-dom": "^18.x", + "styled-components": "^5.x", + "twin.macro": "^3.x" } }, "node_modules/@alloc/quick-lru": { @@ -1561,6 +1565,14 @@ "node": ">=6" } }, + "node_modules/@hookform/resolvers": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.1.tgz", + "integrity": "sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -8490,6 +8502,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.45.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", + "integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10196,6 +10223,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -10291,9 +10326,9 @@ } }, "@aleph-front/aleph-core": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@aleph-front/aleph-core/-/aleph-core-1.3.4.tgz", - "integrity": "sha512-xyKtffq54GcXO/A0QEgx/Z/NqBosH/uZBl0D4/T7WYQRLJn13bLMc83noeFuu9cjxsQH9blPysZlcczFuh3sxw==", + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@aleph-front/aleph-core/-/aleph-core-1.3.13.tgz", + "integrity": "sha512-s3kjq4OAiVKRlS5ZlfNDlpBsdAo6D2aqA/ZMKmLPb7LHFI56iJOEYPKQiSk72AFzn2a2UBnahs9toLy+Ggv5aw==", "requires": { "@monaco-editor/react": "^4.4.6", "react-transition-group": "^4.4.5" @@ -11236,6 +11271,12 @@ "@fortawesome/fontawesome-common-types": "6.4.0" } }, + "@hookform/resolvers": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.1.tgz", + "integrity": "sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -16422,6 +16463,12 @@ "scheduler": "^0.23.0" } }, + "react-hook-form": { + "version": "7.45.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz", + "integrity": "sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A==", + "requires": {} + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -17603,6 +17650,11 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/package.json b/package.json index b215c079..c2d2a205 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "front-aleph-cloud", - "version": "0.2.6", + "version": "0.2.7", "private": true, "scripts": { "dev": "next dev", @@ -11,8 +11,9 @@ "lint:fix": "next lint --fix" }, "dependencies": { - "@aleph-front/aleph-core": "^1.3.4", + "@aleph-front/aleph-core": "^1.3.13", "@fortawesome/fontawesome-svg-core": "^6.3.0", + "@hookform/resolvers": "^3.1.1", "@types/node": "18.14.1", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", @@ -24,11 +25,13 @@ "next": "^13.3.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.45.2", "react-loader-spinner": "^5.3.4", - "styled-components": "^5.3.6", + "styled-components": "^5.3.10", "typescript": "4.9.5", "usehooks-ts": "^2.9.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" }, "devDependencies": { "@babel/plugin-syntax-typescript": "^7.21.4", diff --git a/src/components/common/HiddenFileInput/cmp.tsx b/src/components/common/HiddenFileInput/cmp.tsx index 0837f31a..7e049b97 100644 --- a/src/components/common/HiddenFileInput/cmp.tsx +++ b/src/components/common/HiddenFileInput/cmp.tsx @@ -1,76 +1,85 @@ -import React, { ChangeEvent, memo, useCallback, useRef, useState } from 'react' -import { Button, Icon } from '@aleph-front/aleph-core' +import React, { + ChangeEvent, + ForwardedRef, + forwardRef, + memo, + useCallback, + useRef, +} from 'react' +import { Button, FormError, Icon } from '@aleph-front/aleph-core' import { HiddenFileInputProps } from './types' import { StyledHiddenFileInput } from './styles' import { ellipseAddress } from '@/helpers/utils' export const HiddenFileInput = memo( - ({ onChange, accept, value, children }: HiddenFileInputProps) => { - const inputRef = useRef(null) - const [state, setState] = useState(undefined) + forwardRef( + ( + { onChange, accept, value, children, error }: HiddenFileInputProps, + ref: ForwardedRef, + ) => { + const inputRef = useRef(null) - const file = value || state + const handleClick = useCallback(() => { + if (!inputRef.current) return + inputRef.current.click() + }, []) - const handleClick = useCallback(() => { - if (!inputRef.current) return - inputRef.current.click() - }, []) + const handleRemoveFile = useCallback(() => { + onChange(undefined) + }, [onChange]) - const handleRemoveFile = useCallback(() => { - setState(undefined) - onChange(undefined) - }, [onChange]) + const handleChange = useCallback( + (e: ChangeEvent) => { + // This is verbose to avoid a type error on e.target.files[0] being undefined + const target = e.target as HTMLInputElement + const { files } = target - const handleChange = useCallback( - (e: ChangeEvent) => { - // This is verbose to avoid a type error on e.target.files[0] being undefined - const target = e.target as HTMLInputElement - const { files } = target + if (files) { + const fileUploaded = files[0] + onChange(fileUploaded) + } + }, + [onChange], + ) - if (files) { - const fileUploaded = files[0] - setState(fileUploaded) - onChange(fileUploaded) - } - }, - [onChange], - ) + return ( +
+ {value ? ( + + ) : ( + + )} - return ( - <> - {file ? ( - - ) : ( - - )} + {error && } - - - ) - }, + +
+ ) + }, + ), ) HiddenFileInput.displayName = 'HiddenFileInput' diff --git a/src/components/common/HiddenFileInput/types.tsx b/src/components/common/HiddenFileInput/types.tsx index a919beb0..1db7744a 100644 --- a/src/components/common/HiddenFileInput/types.tsx +++ b/src/components/common/HiddenFileInput/types.tsx @@ -1,6 +1,9 @@ +import { FieldError } from 'react-hook-form' + export type HiddenFileInputProps = { onChange: (files?: File) => void accept?: string value?: File children: React.ReactNode + error?: FieldError } diff --git a/src/components/common/HoldingRequirements/cmp.tsx b/src/components/common/HoldingRequirements/cmp.tsx index cfbe9a69..cfe65e7e 100644 --- a/src/components/common/HoldingRequirements/cmp.tsx +++ b/src/components/common/HoldingRequirements/cmp.tsx @@ -17,6 +17,8 @@ import { InstanceManager } from '@/domain/instance' import { ProgramManager } from '@/domain/program' import { VolumeManager, VolumeType } from '@/domain/volume' import InfoTooltipButton from '../InfoTooltipButton' +import Container from '@/components/common/CenteredContainer' +import { TextGradient } from '@aleph-front/aleph-core' const HoldingRequirementsSpecsLine = React.memo( ({ type, specs, cost }: HoldingRequirementsSpecsLineProps) => { @@ -71,6 +73,7 @@ HoldingRequirementsSpecsLine.displayName = 'HoldingRequirementsSpecsLine' const HoldingRequirementsVolumeLine = React.memo( ({ volume, cost, specs }: HoldingRequirementsVolumeLineProps) => { + const size = VolumeManager.getVolumeSize(volume) if (!cost) return <> const hasDiscount = !!cost.discount @@ -90,7 +93,7 @@ const HoldingRequirementsVolumeLine = React.memo(
- {convertBitUnits(volume.size || 0, { + {convertBitUnits(size, { from: 'mb', to: 'gb', displayUnit: true, @@ -178,6 +181,8 @@ export default function HoldingRequirements({ volumes, domains, isPersistent = type === EntityType.Instance, + button: ButtonCmp, + description, }: HoldingRequirementsProps) { const { computeTotalCost, perVolumeCost, totalCost } = useMemo(() => { switch (type) { @@ -203,63 +208,89 @@ export default function HoldingRequirements({ }, [isPersistent, specs, type, volumes]) return ( -
- -
UNLOCKED
-
current wallet {ellipseAddress(address)}
-
{humanReadableCurrency(unlockedAmount)} ALEPH
-
+ <> +
+
+ + + Estimated holding requirements + + {description && ( +
+

{description}

+
+ )} +
+
+ +
UNLOCKED
+
+ current wallet {ellipseAddress(address)} +
+
{humanReadableCurrency(unlockedAmount)} ALEPH
+
- {specs && ( - - )} + {specs && ( + + )} - {volumes && - volumes.map((volume) => { - return ( - - ) - })} + {volumes && + volumes.map((volume, index) => { + return ( + + ) + })} - {type === EntityType.Program && ( - -
TYPE
-
{isPersistent ? 'persistent' : 'on-demand'}
-
-
-
- )} + {type === EntityType.Program && ( + +
TYPE
+
{isPersistent ? 'persistent' : 'on-demand'}
+
-
+
+ )} - {domains && - domains.map((domain) => { - return ( - - ) - })} + {domains && + domains.map((domain) => { + return ( + + ) + })} - -
-
Total
-
- - {humanReadableCurrency(totalCost)} ALEPH - -
-
-
+ +
+
Total
+
+ + {humanReadableCurrency(totalCost)} ALEPH + +
+
+
+
+ {ButtonCmp &&
{ButtonCmp}
} + + + ) } diff --git a/src/components/common/HoldingRequirements/types.ts b/src/components/common/HoldingRequirements/types.ts index e705c907..19682897 100644 --- a/src/components/common/HoldingRequirements/types.ts +++ b/src/components/common/HoldingRequirements/types.ts @@ -1,31 +1,34 @@ import { PerVolumeCostItem } from '@/domain/volume' import { EntityType } from '@/helpers/constants' -import { DomainProp } from '@/hooks/form/useAddDomains' -import { VolumeProp } from '@/hooks/form/useAddVolume' -import { InstanceSpecsProp } from '@/hooks/form/useSelectInstanceSpecs' +import { DomainField } from '@/hooks/form/useAddDomains' +import { VolumeField } from '@/hooks/form/useAddVolume' +import { InstanceSpecsField } from '@/hooks/form/useSelectInstanceSpecs' +import { ReactNode } from 'react' export type HoldingRequirementsProps = { address: string unlockedAmount: number type: EntityType.Program | EntityType.Instance | EntityType.Volume isPersistent?: boolean - specs?: InstanceSpecsProp - volumes?: VolumeProp[] - domains?: DomainProp[] + specs?: InstanceSpecsField + volumes?: VolumeField[] + domains?: DomainField[] + button?: ReactNode + description?: ReactNode } export type HoldingRequirementsSpecsLineProps = { type: EntityType.Program | EntityType.Instance | EntityType.Volume - specs: InstanceSpecsProp + specs: InstanceSpecsField cost: number } export type HoldingRequirementsVolumeLineProps = { - volume: VolumeProp - specs?: InstanceSpecsProp + volume: VolumeField + specs?: InstanceSpecsField cost?: PerVolumeCostItem } export type HoldingRequirementsDomainLineProps = { - domain: DomainProp + domain: DomainField } diff --git a/src/components/common/InfoTooltipButton/cmp.tsx b/src/components/common/InfoTooltipButton/cmp.tsx index 58eb35c2..1ecac766 100644 --- a/src/components/common/InfoTooltipButton/cmp.tsx +++ b/src/components/common/InfoTooltipButton/cmp.tsx @@ -33,12 +33,15 @@ export default function InfoTooltipButton({ return ( <> {plain ? ( - + {children} {iconElm} ) : ( - + {children} {iconElm} diff --git a/src/components/common/Main/cmp.tsx b/src/components/common/Main/cmp.tsx index 0003e367..9372984e 100644 --- a/src/components/common/Main/cmp.tsx +++ b/src/components/common/Main/cmp.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { MainProps } from './types' import { useConnect } from '@/hooks/common/useConnect' +import { StyledMain } from './styles' export const Main = ({ children }: MainProps) => { const { tryReconnect } = useConnect() @@ -9,7 +10,7 @@ export const Main = ({ children }: MainProps) => { tryReconnect() }, [tryReconnect]) - return
{children}
+ return {children} } export default Main diff --git a/src/components/common/Main/styles.tsx b/src/components/common/Main/styles.tsx new file mode 100644 index 00000000..355e94eb --- /dev/null +++ b/src/components/common/Main/styles.tsx @@ -0,0 +1,6 @@ +import styled from 'styled-components' +import tw from 'twin.macro' + +export const StyledMain = styled.main` + ${tw`flex flex-col flex-1`} +` diff --git a/src/components/form/AddDomains/cmp.tsx b/src/components/form/AddDomains/cmp.tsx index 9e88fa1c..ae77c5ef 100644 --- a/src/components/form/AddDomains/cmp.tsx +++ b/src/components/form/AddDomains/cmp.tsx @@ -5,16 +5,15 @@ import { DomainItemProps, AddDomainsProps as AddDomainsProps } from './types' import NoisyContainer from '@/components/common/NoisyContainer' const DomainItem = React.memo((props: DomainItemProps) => { - const { id, domain, handleNameChange, handleRemove } = useDomainItem(props) + const { nameCtrl, handleRemove } = useDomainItem(props) return (
@@ -34,47 +33,47 @@ const DomainItem = React.memo((props: DomainItemProps) => { }) DomainItem.displayName = 'DomainItem' -export const AddDomains = React.memo( - ({ domains: domainsProp, onChange }: AddDomainsProps) => { - const { domains, handleChange, handleAdd, handleRemove } = useAddDomains({ - domains: domainsProp, - onChange, - }) +export const AddDomains = React.memo((props: AddDomainsProps) => { + const { name, control, fields, handleAdd, handleRemove } = + useAddDomains(props) - return ( - <> - {domains.length > 0 && ( - -
- {domains.map((domain) => ( - - ))} -
-
- )} - {domains.length < 1 && ( -
- + return ( + <> + {fields.length > 0 && ( + +
+ {fields.map((field, index) => ( + + ))}
- )} - - ) - }, -) +
+ )} + {fields.length < 1 && ( +
+ +
+ )} + + ) +}) AddDomains.displayName = 'AddDomains' export default AddDomains diff --git a/src/components/form/AddDomains/types.ts b/src/components/form/AddDomains/types.ts index adacf82e..f5b4a696 100644 --- a/src/components/form/AddDomains/types.ts +++ b/src/components/form/AddDomains/types.ts @@ -1,12 +1,15 @@ -import { DomainProp } from '@/hooks/form/useAddDomains' +import { EntityType } from '@/helpers/constants' +import { Control } from 'react-hook-form' export type DomainItemProps = { - domain: DomainProp - onChange: (domain: DomainProp) => void - onRemove: (domainId: string) => void + name?: string + index: number + control: Control + onRemove: (index?: number) => void } export type AddDomainsProps = { - domains?: DomainProp[] - onChange: (domains: DomainProp[]) => void + control: Control + name: string + entityType: EntityType.Program | EntityType.Instance } diff --git a/src/components/form/AddEnvVars/cmp.tsx b/src/components/form/AddEnvVars/cmp.tsx index 203b50cc..c79de365 100644 --- a/src/components/form/AddEnvVars/cmp.tsx +++ b/src/components/form/AddEnvVars/cmp.tsx @@ -5,25 +5,22 @@ import { EnvVarItemProps, AddEnvVarsProps as AddEnvVarsProps } from './types' import NoisyContainer from '@/components/common/NoisyContainer' const EnvVarItem = React.memo((props: EnvVarItemProps) => { - const { id, envVar, handleNameChange, handleValueChange, handleRemove } = - useEnvVarItem(props) + const { nameCtrl, valueCtrl, handleRemove } = useEnvVarItem(props) return (
@@ -43,46 +40,46 @@ const EnvVarItem = React.memo((props: EnvVarItemProps) => { }) EnvVarItem.displayName = 'EnvVarItem' -export const AddEnvVars = React.memo( - ({ envVars: envVarsProp, onChange }: AddEnvVarsProps) => { - const { envVars, handleChange, handleAdd, handleRemove } = useAddEnvVars({ - envVars: envVarsProp, - onChange, - }) +export const AddEnvVars = React.memo((props: AddEnvVarsProps) => { + const { name, control, fields, handleAdd, handleRemove } = + useAddEnvVars(props) - return ( - <> - {envVars.length > 0 && ( - -
-

Set

- {envVars.map((envVar) => ( - - ))} -
-
- )} -
- -
- - ) - }, -) + return ( + <> + {fields.length > 0 && ( + +
+

Set

+ {fields.map((field, index) => ( + + ))} +
+
+ )} +
+ +
+ + ) +}) AddEnvVars.displayName = 'AddEnvVars' export default AddEnvVars diff --git a/src/components/form/AddEnvVars/types.ts b/src/components/form/AddEnvVars/types.ts index 7fb3ca5b..1dfdd51a 100644 --- a/src/components/form/AddEnvVars/types.ts +++ b/src/components/form/AddEnvVars/types.ts @@ -1,12 +1,13 @@ -import { EnvVarProp } from '@/hooks/form/useAddEnvVars' +import { Control } from 'react-hook-form' export type EnvVarItemProps = { - envVar: EnvVarProp - onChange: (envVar: EnvVarProp) => void - onRemove: (envVarId: string) => void + name?: string + index: number + control: Control + onRemove: (index?: number) => void } export type AddEnvVarsProps = { - envVars?: EnvVarProp[] - onChange: (envVars: EnvVarProp[]) => void + name: string + control: Control } diff --git a/src/components/form/AddFunctionCode/cmp.tsx b/src/components/form/AddFunctionCode/cmp.tsx new file mode 100644 index 00000000..1f26d71e --- /dev/null +++ b/src/components/form/AddFunctionCode/cmp.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import { AddFunctionCodeProps } from './types' +import HiddenFileInput from '@/components/common/HiddenFileInput' +import InfoTooltipButton from '@/components/common/InfoTooltipButton' +import { + CodeEditor, + Icon, + Radio, + RadioGroup, + Tabs, +} from '@aleph-front/aleph-core' +import NoisyContainer from '@/components/common/NoisyContainer' +import { useAddFunctionCode } from '@/hooks/form/useAddFunctionCode' + +export const AddFunctionCode = React.memo((props: AddFunctionCodeProps) => { + const { langCtrl, typeCtrl, textCtrl, fileCtrl } = useAddFunctionCode(props) + + return ( + <> +
+ +
+
+ {typeCtrl.field.value === 'text' ? ( + <> +

+ To get started you can start adding your code in the window below. +

+
+ + + + + + +
+
+ +
+
+ +
+
Write code
+
+ Your code should have an app function that will serve as + an entrypoint to the program. +
+
+
+
Upload code
+
+ Your zip file should contain a main file (ex: main.py) + at its root that exposes an app function. This will + serve as an entrypoint to the program. +
+
+
+ } + > + Learn more + +
+ + ) : ( + <> +

+ To get started, compress your code into a zip file and upload it + here. +

+

+ Your zip archive should contain a{' '} + main file (ex: main.py) at its + root that exposes an app function. + This will serve as an entrypoint to the program +

+ +
+ + Upload code + +
+
+ + )} +
+ + ) +}) +AddFunctionCode.displayName = 'AddFunctionCode' + +export default AddFunctionCode diff --git a/src/components/form/AddFunctionCode/index.ts b/src/components/form/AddFunctionCode/index.ts new file mode 100644 index 00000000..7a9f83f3 --- /dev/null +++ b/src/components/form/AddFunctionCode/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/form/AddFunctionCode/types.ts b/src/components/form/AddFunctionCode/types.ts new file mode 100644 index 00000000..768f64e4 --- /dev/null +++ b/src/components/form/AddFunctionCode/types.ts @@ -0,0 +1,6 @@ +import { Control } from 'react-hook-form' + +export type AddFunctionCodeProps = { + name?: string + control: Control +} diff --git a/src/components/form/AddNameAndTags/cmp.tsx b/src/components/form/AddNameAndTags/cmp.tsx index f41bf6dd..2d8aa034 100644 --- a/src/components/form/AddNameAndTags/cmp.tsx +++ b/src/components/form/AddNameAndTags/cmp.tsx @@ -6,8 +6,7 @@ import NoisyContainer from '@/components/common/NoisyContainer' import { AddNameAndTagsProps } from './types' export const AddNameAndTags = React.memo((props: AddNameAndTagsProps) => { - const { entityName, name, tags, handleNameChange, handleTagsChange } = - useAddNameAndTags(props) + const { entityName, nameCtrl, tagsCtrl } = useAddNameAndTags(props) return ( @@ -35,10 +34,9 @@ export const AddNameAndTags = React.memo((props: AddNameAndTagsProps) => {
@@ -66,10 +64,9 @@ export const AddNameAndTags = React.memo((props: AddNameAndTagsProps) => {
diff --git a/src/components/form/AddNameAndTags/types.ts b/src/components/form/AddNameAndTags/types.ts index a0b7a00d..84bdc764 100644 --- a/src/components/form/AddNameAndTags/types.ts +++ b/src/components/form/AddNameAndTags/types.ts @@ -1,7 +1,8 @@ import { EntityType } from '@/helpers/constants' -import { NameAndTagsProp } from '@/hooks/form/useAddNameAndTags' +import { Control } from 'react-hook-form' -export type AddNameAndTagsProps = NameAndTagsProp & { +export type AddNameAndTagsProps = { entityType: EntityType.Instance | EntityType.Program - onChange: (state: NameAndTagsProp) => void + name?: string + control: Control } diff --git a/src/components/form/AddSSHKeys/cmp.tsx b/src/components/form/AddSSHKeys/cmp.tsx index cca79737..a0b8124a 100644 --- a/src/components/form/AddSSHKeys/cmp.tsx +++ b/src/components/form/AddSSHKeys/cmp.tsx @@ -1,53 +1,61 @@ import React from 'react' -import { Icon, TextInput, Button, Checkbox } from '@aleph-front/aleph-core' +import { + Icon, + TextInput, + Button, + Checkbox, + FormError, +} from '@aleph-front/aleph-core' import { useAddSSHKeys, useSSHKeyItem } from '@/hooks/form/useAddSSHKeys' import { SSHKeyItemProps, AddSSHKeysProps } from './types' import NoisyContainer from '@/components/common/NoisyContainer' -const SSHKeyItem = React.memo( - ({ index, allowRemove, ...props }: SSHKeyItemProps) => { - const { - id, - sshKey, - handleIsSelectedChange, - handleKeyChange, - handleLabelChange, - handleRemove, - } = useSSHKeyItem(props) +const SSHKeyItem = React.memo((props: SSHKeyItemProps) => { + const { + index, + keyCtrl, + labelCtrl, + isSelectedCtrl, + allowRemove, + isNew, + handleRemove, + } = useSSHKeyItem(props) - return ( + return ( + <> + {isSelectedCtrl.fieldState.error && ( + + )}
{allowRemove && (
- {sshKey.isNew && ( + {isNew && (
- ) - }, -) + + ) +}) SSHKeyItem.displayName = 'SSHKeyItem' -export const AddSSHKeys = React.memo( - ({ sshKeys: sshKeysProp, onChange }: AddSSHKeysProps) => { - const { sshKeys, handleChange, handleAdd, handleRemove, allowRemove } = - useAddSSHKeys({ - sshKeys: sshKeysProp, - onChange, - }) +export const AddSSHKeys = React.memo((props: AddSSHKeysProps) => { + const { name, control, fields, handleAdd, handleRemove, allowRemove } = + useAddSSHKeys(props) - return ( - <> - {sshKeys.length > 0 && ( - -
- {sshKeys.map((sshKey, index) => ( - - ))} -
-
- )} -
- -
- - ) - }, -) + return ( + <> + {fields.length > 0 && ( + +
+ {fields.map((field, index) => ( + + ))} +
+
+ )} +
+ +
+ + ) +}) AddSSHKeys.displayName = 'AddSSHKeys' export default AddSSHKeys diff --git a/src/components/form/AddSSHKeys/types.ts b/src/components/form/AddSSHKeys/types.ts index 81a85724..78be7865 100644 --- a/src/components/form/AddSSHKeys/types.ts +++ b/src/components/form/AddSSHKeys/types.ts @@ -1,14 +1,16 @@ -import { SSHKeyProp } from '@/hooks/form/useAddSSHKeys' +import { SSHKeyField } from '@/hooks/form/useAddSSHKeys' +import { Control } from 'react-hook-form' export type SSHKeyItemProps = { - sshKey: SSHKeyProp - onChange: (sshKey: SSHKeyProp) => void - onRemove: (sshKeyId: string) => void + name?: string index: number + control: Control allowRemove: boolean + defaultValue: SSHKeyField + onRemove: (index?: number) => void } export type AddSSHKeysProps = { - sshKeys?: SSHKeyProp[] - onChange: (sshKeys: SSHKeyProp[]) => void + name: string + control: Control } diff --git a/src/components/form/AddVolume/cmp.tsx b/src/components/form/AddVolume/cmp.tsx index 4b8cd30f..e011f88c 100644 --- a/src/components/form/AddVolume/cmp.tsx +++ b/src/components/form/AddVolume/cmp.tsx @@ -1,4 +1,3 @@ -import { isValidItemHash } from '@/helpers/utils' import { Tabs, Icon, @@ -13,48 +12,44 @@ import { AddNewVolumeProps, AddPersistentVolumeProps, } from './types' -import React, { useCallback, useMemo } from 'react' +import React, { useMemo } from 'react' import { useAddVolume, useAddExistingVolumeProps, useAddNewVolumeProps, useAddPersistentVolumeProps, } from '@/hooks/form/useAddVolume' -import { VolumeType, Volume } from '@/domain/volume' +import { VolumeType } from '@/domain/volume' import NoisyContainer from '@/components/common/NoisyContainer' import HiddenFileInput from '@/components/common/HiddenFileInput' -const RemoveVolume = React.memo(({ volume, onRemove }: RemoveVolumeProps) => { - const handleRemove = useCallback(() => { - onRemove(volume.id) - }, [onRemove, volume.id]) - - return ( -
- -
- ) -}) +const RemoveVolume = React.memo( + ({ onRemove: handleRemove }: RemoveVolumeProps) => { + return ( +
+ +
+ ) + }, +) RemoveVolume.displayName = 'RemoveVolume' export const AddNewVolume = React.memo((props: AddNewVolumeProps) => { const { - id, - volume, - volumeSize, isStandAlone, - handleFileSrcChange, - handleMountPathChange, - handleUseLatestChange, + fileCtrl, + mountPathCtrl, + useLatestCtrl, + volumeSize, handleRemove, } = useAddNewVolumeProps(props) @@ -68,46 +63,35 @@ export const AddNewVolume = React.memo((props: AddNewVolumeProps) => {

- + Upload squashfs volume
{!isStandAlone && (
)} - {volume.fileSrc && ( + {fileCtrl.field.value && (
- +
)} {!isStandAlone && ( <>
- {handleRemove && ( - - )} + {handleRemove && } )}
@@ -117,14 +101,8 @@ export const AddNewVolume = React.memo((props: AddNewVolumeProps) => { AddNewVolume.displayName = 'AddNewVolume' const AddExistingVolume = React.memo((props: AddExistingVolumeProps) => { - const { - id, - volume, - handleRefHashChange, - handleMountPathChange, - handleUseLatestChange, - handleRemove, - } = useAddExistingVolumeProps(props) + const { refHashCtrl, mountPathCtrl, useLatestCtrl, handleRemove } = + useAddExistingVolumeProps(props) return ( <> @@ -137,37 +115,29 @@ const AddExistingVolume = React.memo((props: AddExistingVolumeProps) => {
- {handleRemove && ( - - )} + {handleRemove && }
) @@ -176,12 +146,11 @@ AddExistingVolume.displayName = 'AddExistingVolume' const AddPersistentVolume = React.memo((props: AddPersistentVolumeProps) => { const { - id, - volume, - volumeSize, - handleNameChange, - handleMountPathChange, - handleSizeChange, + nameCtrl, + mountPathCtrl, + sizeCtrl, + sizeValue, + sizeHandleChange, handleRemove, } = useAddPersistentVolumeProps(props) @@ -196,39 +165,32 @@ const AddPersistentVolume = React.memo((props: AddPersistentVolumeProps) => {
- {handleRemove && ( - - )} + {handleRemove && }
) @@ -241,59 +203,40 @@ const CmpMap = { [VolumeType.Persistent]: AddPersistentVolume, } -export const AddVolume = React.memo( - ({ isStandAlone, volume: volumeProp, onChange, ...rest }: AddVolumeProps) => { - const { volume, handleChange } = useAddVolume({ - volume: volumeProp, - onChange, - }) +export const AddVolume = React.memo((props: AddVolumeProps) => { + const { volumeTypeCtrl, ...rest } = useAddVolume(props) + const volumeType = volumeTypeCtrl.field.value as VolumeType - const handleVolumeTypeChange = useCallback( - (type: string | VolumeType) => { - const newVolume = { ...volume, volumeType: type } as Volume - handleChange(newVolume) - }, - [handleChange, volume], - ) - - const Cmp = useMemo(() => CmpMap[volume.volumeType], [volume.volumeType]) - - if (isStandAlone) - return ( - - ) - - return ( - <> -
- -
+ const Cmp = useMemo(() => CmpMap[volumeType], [volumeType]) -
- {} -
- - ) - }, -) + return ( + <> +
+ +
+ +
{}
+ + ) +}) AddVolume.displayName = 'AddVolume' export default AddVolume diff --git a/src/components/form/AddVolume/types.ts b/src/components/form/AddVolume/types.ts index ce01e561..b6ad6441 100644 --- a/src/components/form/AddVolume/types.ts +++ b/src/components/form/AddVolume/types.ts @@ -1,37 +1,43 @@ import { - ExistingVolumeProp, - NewVolumeProp, - PersistentVolumeProp, - VolumeProp, + ExistingVolumeField, + NewVolumeField, + PersistentVolumeField, + VolumeField, } from '@/hooks/form/useAddVolume' +import { Control } from 'react-hook-form' -export type RemoveVolumeProps = { - volume: VolumeProp - onRemove: (volumeId: string) => void +export type AddVolumeCommonProps = { + name?: string + index?: number + control: Control + onRemove?: (index?: number) => void +} + +export type AddNewVolumeStandaloneProps = Omit< + AddVolumeCommonProps, + 'onRemove' | 'index' +> & { + defaultValue?: NewVolumeField } -export type AddNewVolumeProps = { - volume: NewVolumeProp - isStandAlone?: boolean - onChange: (volume: NewVolumeProp) => void - onRemove?: (volumeId: string) => void +export type AddNewVolumeProps = AddVolumeCommonProps & { + defaultValue?: NewVolumeField } -export type AddExistingVolumeProps = { - volume: ExistingVolumeProp - onChange: (volume: ExistingVolumeProp) => void - onRemove?: (volumeId: string) => void +export type AddExistingVolumeProps = AddVolumeCommonProps & { + defaultValue?: ExistingVolumeField } -export type AddPersistentVolumeProps = { - volume: PersistentVolumeProp - onChange: (volume: PersistentVolumeProp) => void - onRemove?: (volumeId: string) => void +export type AddPersistentVolumeProps = AddVolumeCommonProps & { + defaultValue?: PersistentVolumeField +} + +// --------- + +export type RemoveVolumeProps = { + onRemove: () => void } -export type AddVolumeProps = { - volume: VolumeProp - isStandAlone?: boolean - onChange: (volume: VolumeProp) => void - onRemove?: (volumeId: string) => void +export type AddVolumeProps = AddVolumeCommonProps & { + defaultValue?: VolumeField } diff --git a/src/components/form/AddVolumes/cmp.tsx b/src/components/form/AddVolumes/cmp.tsx index ffa380e1..eaf73d1a 100644 --- a/src/components/form/AddVolumes/cmp.tsx +++ b/src/components/form/AddVolumes/cmp.tsx @@ -6,17 +6,21 @@ import AddVolume from '../AddVolume' import InfoTooltipButton from '@/components/common/InfoTooltipButton' export const AddVolumes = React.memo((props: AddVolumesProps) => { - const { volumes, handleAdd, handleRemove, handleChange } = + const { name, control, fields, handleAdd, handleRemove } = useAddVolumes(props) return ( <> - {volumes.map((volume) => ( + {fields.map((field, index) => ( ))}
@@ -28,7 +32,7 @@ export const AddVolumes = React.memo((props: AddVolumesProps) => { kind="neon" size="regular" > - {volumes.length > 0 ? 'Add another volume' : 'Add volume'} + {fields.length > 0 ? 'Add another volume' : 'Add volume'}
diff --git a/src/components/form/AddVolumes/types.ts b/src/components/form/AddVolumes/types.ts index b6e90814..23f7bba5 100644 --- a/src/components/form/AddVolumes/types.ts +++ b/src/components/form/AddVolumes/types.ts @@ -1,10 +1,10 @@ -import { VolumeProp } from '@/hooks/form/useAddVolume' +import { Control } from 'react-hook-form' export type RemoveVolumeProps = { onRemove: () => void } export type AddVolumesProps = { - volumes?: VolumeProp[] - onChange: (volumes: VolumeProp[]) => void + name?: string + control: Control } & Partial diff --git a/src/components/form/Form/cmp.tsx b/src/components/form/Form/cmp.tsx new file mode 100644 index 00000000..8410feaf --- /dev/null +++ b/src/components/form/Form/cmp.tsx @@ -0,0 +1,20 @@ +import { FormProps } from './types' +import { StyledForm } from './styles' +import { FormError, FormErrorProps } from '@aleph-front/aleph-core' +import Container from '@/components/common/CenteredContainer' + +export const Form = ({ children, onSubmit, errors }: FormProps) => { + return ( + + {children} + + {errors?.root && + Object.values(errors.root).map((error) => ( + + ))} + + + ) +} + +export default Form diff --git a/src/components/form/Form/index.ts b/src/components/form/Form/index.ts new file mode 100644 index 00000000..cd97708f --- /dev/null +++ b/src/components/form/Form/index.ts @@ -0,0 +1,2 @@ +export { default } from './cmp' +export type { FormProps } from './types' diff --git a/src/components/form/Form/styles.tsx b/src/components/form/Form/styles.tsx new file mode 100644 index 00000000..4fb14e50 --- /dev/null +++ b/src/components/form/Form/styles.tsx @@ -0,0 +1,6 @@ +import styled from 'styled-components' +import tw from 'twin.macro' + +export const StyledForm = styled.form` + ${tw`flex flex-col flex-1`} +` diff --git a/src/components/form/Form/types.ts b/src/components/form/Form/types.ts new file mode 100644 index 00000000..c1dff978 --- /dev/null +++ b/src/components/form/Form/types.ts @@ -0,0 +1,8 @@ +import { FormEvent, ReactNode } from 'react' +import { FieldErrors } from 'react-hook-form' + +export type FormProps = { + children: ReactNode + onSubmit: (e: FormEvent) => Promise + errors: FieldErrors +} diff --git a/src/components/form/SelectFunctionPersistence/cmp.tsx b/src/components/form/SelectFunctionPersistence/cmp.tsx new file mode 100644 index 00000000..6495c4bd --- /dev/null +++ b/src/components/form/SelectFunctionPersistence/cmp.tsx @@ -0,0 +1,39 @@ +/* eslint-disable @next/next/no-img-element */ +import React from 'react' +import { useSelectFunctionPersistence } from '@/hooks/form/useSelectFunctionPersistence' +import { Radio, RadioGroup } from '@aleph-front/aleph-core' +import NoisyContainer from '@/components/common/NoisyContainer' +import ExternalLinkButton from '@/components/common/ExternalLinkButton' +import { SelectFunctionPersistenceProps } from './types' + +export const SelectFunctionPersistence = React.memo( + (props: SelectFunctionPersistenceProps) => { + const { isPersistentValue, isPersistentHandleChange, isPersistentCtrl } = + useSelectFunctionPersistence(props) + + return ( + <> + + + + + + +
+ + Learn more + +
+ + ) + }, +) +SelectFunctionPersistence.displayName = 'SelectFunctionPersistence' + +export default SelectFunctionPersistence diff --git a/src/components/form/SelectFunctionPersistence/index.ts b/src/components/form/SelectFunctionPersistence/index.ts new file mode 100644 index 00000000..f15a3ab0 --- /dev/null +++ b/src/components/form/SelectFunctionPersistence/index.ts @@ -0,0 +1,2 @@ +export { default } from './cmp' +export type { SelectFunctionPersistenceProps } from './types' diff --git a/src/components/form/SelectFunctionPersistence/types.ts b/src/components/form/SelectFunctionPersistence/types.ts new file mode 100644 index 00000000..079249e4 --- /dev/null +++ b/src/components/form/SelectFunctionPersistence/types.ts @@ -0,0 +1,6 @@ +import { Control } from 'react-hook-form' + +export type SelectFunctionPersistenceProps = { + name?: string + control: Control +} diff --git a/src/components/form/SelectFunctionRuntime/cmp.tsx b/src/components/form/SelectFunctionRuntime/cmp.tsx index 64b7c865..13316c80 100644 --- a/src/components/form/SelectFunctionRuntime/cmp.tsx +++ b/src/components/form/SelectFunctionRuntime/cmp.tsx @@ -1,84 +1,43 @@ -/* eslint-disable @next/next/no-img-element */ import React from 'react' -import { useCallback, useId } from 'react' import { useSelectFunctionRuntime } from '@/hooks/form/useSelectFunctionRuntime' import { Radio, RadioGroup, TextInput } from '@aleph-front/aleph-core' import NoisyContainer from '@/components/common/NoisyContainer' import ExternalLinkButton from '@/components/common/ExternalLinkButton' -import { isValidItemHash } from '@/helpers/utils' -import { - SelectFunctionRuntimeItemProps, - SelectFunctionRuntimeProps, -} from './types' -import { FunctionRuntimeId } from '@/domain/runtime' - -const SelectFunctionRuntimeItem = React.memo( - ({ runtime, selected, onChange }: SelectFunctionRuntimeItemProps) => { - const id = useId() - - const handleChange = useCallback(() => { - onChange(runtime) - }, [runtime, onChange]) - - return ( - - ) - }, -) -SelectFunctionRuntimeItem.displayName = 'SelectFunctionRuntimeItem' +import { SelectFunctionRuntimeProps } from './types' export const SelectFunctionRuntime = React.memo( (props: SelectFunctionRuntimeProps) => { - const { - runtime, - options, - handleRuntimeChange, - handleCustomRuntimeHashChange, - } = useSelectFunctionRuntime(props) + const { customCtrl, idCtrl, isCustomDisabled, options } = + useSelectFunctionRuntime(props) return ( <> - + {options.map((option) => ( - ))} - -
- -
+ {!isCustomDisabled && ( +
+ +
+ )}
diff --git a/src/components/form/SelectFunctionRuntime/types.ts b/src/components/form/SelectFunctionRuntime/types.ts index d8612347..e39d98c6 100644 --- a/src/components/form/SelectFunctionRuntime/types.ts +++ b/src/components/form/SelectFunctionRuntime/types.ts @@ -1,14 +1,8 @@ -import { FunctionRuntimeProp } from '@/hooks/form/useSelectFunctionRuntime' - -export type SelectFunctionRuntimeItemProps = { - runtime: FunctionRuntimeProp - selected: boolean - onChange: (runtime: FunctionRuntimeProp) => void -} +import { FunctionRuntime } from '@/domain/runtime' +import { Control } from 'react-hook-form' export type SelectFunctionRuntimeProps = { - runtime?: FunctionRuntimeProp - customRuntimeHash?: string - options?: FunctionRuntimeProp[] - onChange: (runtime: FunctionRuntimeProp) => void + name?: string + control: Control + options?: FunctionRuntime[] } diff --git a/src/components/form/SelectInstanceImage/cmp.tsx b/src/components/form/SelectInstanceImage/cmp.tsx index 53c72244..1eb22b93 100644 --- a/src/components/form/SelectInstanceImage/cmp.tsx +++ b/src/components/form/SelectInstanceImage/cmp.tsx @@ -1,63 +1,81 @@ /* eslint-disable @next/next/no-img-element */ +import React, { KeyboardEvent } from 'react' import { useBasePath } from '@/hooks/common/useBasePath' -import { useCallback } from 'react' +import { ForwardedRef, forwardRef, memo, useCallback } from 'react' import { useSelectInstanceImage } from '@/hooks/form/useSelectInstanceImage' import { SelectInstanceImageItemProps, SelectInstanceImageProps } from './types' import { StyledFlatCard, StyledFlatCardContainer } from './styles' -import React from 'react' +import { FormError } from '@aleph-front/aleph-core' -const SelectInstanceImageItem = React.memo( - ({ image, selected, onChange }: SelectInstanceImageItemProps) => { - const basePath = useBasePath() - const imgPrefix = `${basePath}/img` +const SelectInstanceImageItem = memo( + forwardRef( + ( + { option, index, value, onChange }: SelectInstanceImageItemProps, + ref: ForwardedRef, + ) => { + const selected = value === option.id + const basePath = useBasePath() + const imgPrefix = `${basePath}/img` - const handleClick = useCallback(() => { - if (image.disabled) return + const handleClick = useCallback(() => { + if (option.disabled) return + onChange(option.id) + }, [option, onChange]) - onChange(image) - }, [image, onChange]) + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.code !== 'Space' && e.code !== 'Enter') return + e.preventDefault() + onChange(option.id) + }, + [option, onChange], + ) - return ( - - {`${image.name} - {image.name} - - ) - }, + return ( + + {`${option.name} + {option.name} + + ) + }, + ), ) SelectInstanceImageItem.displayName = 'SelectInstanceImageItem' -export const SelectInstanceImage = React.memo( - (props: SelectInstanceImageProps) => { - const { image, options, handleChange } = useSelectInstanceImage(props) +export const SelectInstanceImage = memo((props: SelectInstanceImageProps) => { + const { imageCtrl, options } = useSelectInstanceImage(props) - return ( -
- - {options.map((option) => ( - - ))} - -
- ) - }, -) + return ( +
+ + {options.map((option, index) => ( + + ))} + + {imageCtrl.fieldState.error && ( + + )} +
+ ) +}) SelectInstanceImage.displayName = 'SelectInstanceImage' export default SelectInstanceImage diff --git a/src/components/form/SelectInstanceImage/styles.tsx b/src/components/form/SelectInstanceImage/styles.tsx index 075cfb5b..b0ca920d 100644 --- a/src/components/form/SelectInstanceImage/styles.tsx +++ b/src/components/form/SelectInstanceImage/styles.tsx @@ -3,7 +3,7 @@ import tw from 'twin.macro' import { getResponsiveCss } from '@aleph-front/aleph-core' export const StyledFlatCardContainer = styled.div` - ${tw`flex items-center justify-start md:justify-center flex-nowrap md:flex-wrap gap-6`} + ${tw`flex items-center justify-start md:justify-evenly flex-nowrap md:flex-wrap gap-6`} ` export const StyledFlatCard = styled.div<{ diff --git a/src/components/form/SelectInstanceImage/types.ts b/src/components/form/SelectInstanceImage/types.ts index c41f4a81..5d7ee4d7 100644 --- a/src/components/form/SelectInstanceImage/types.ts +++ b/src/components/form/SelectInstanceImage/types.ts @@ -1,13 +1,16 @@ -import { InstanceImageProp } from '@/hooks/form/useSelectInstanceImage' +import { InstanceImage } from '@/domain/image' +import { InstanceImageField } from '@/hooks/form/useSelectInstanceImage' +import { Control } from 'react-hook-form' export type SelectInstanceImageItemProps = { - image: InstanceImageProp - selected: boolean - onChange: (image: InstanceImageProp) => void + option: InstanceImage + index: number + value?: InstanceImageField + onChange: (value: InstanceImageField) => void } export type SelectInstanceImageProps = { - image?: InstanceImageProp - options?: InstanceImageProp[] - onChange: (image: InstanceImageProp) => void + name?: string + control: Control + options?: InstanceImage[] } diff --git a/src/components/form/SelectInstanceSpecs/cmp.tsx b/src/components/form/SelectInstanceSpecs/cmp.tsx index 275a78c5..988ae019 100644 --- a/src/components/form/SelectInstanceSpecs/cmp.tsx +++ b/src/components/form/SelectInstanceSpecs/cmp.tsx @@ -1,7 +1,7 @@ -import React from 'react' +import React, { KeyboardEvent, memo } from 'react' /* eslint-disable @next/next/no-img-element */ import { useSelectInstanceSpecs } from '@/hooks/form/useSelectInstanceSpecs' -import { Button, Icon, TableColumn } from '@aleph-front/aleph-core' +import { Button, FormError, Icon, TableColumn } from '@aleph-front/aleph-core' import { useCallback, useMemo } from 'react' import { convertBitUnits } from '@/helpers/utils' import { SelectInstanceSpecsProps, SpecsDetail } from './types' @@ -9,134 +9,142 @@ import { StyledTable } from './styles' import { Executable } from '@/domain/executable' import { EntityType } from '@/helpers/constants' -export const SelectInstanceSpecs = React.memo( - (props: SelectInstanceSpecsProps) => { - const { type, specs, options, isPersistent, handleChange } = - useSelectInstanceSpecs(props) +export const SelectInstanceSpecs = memo((props: SelectInstanceSpecsProps) => { + const { specsCtrl, options, type, isPersistent } = + useSelectInstanceSpecs(props) - const columns = useMemo(() => { - const cols = [ - { - label: 'Cores', - sortable: true, - sortBy: (row: SpecsDetail) => row.specs.cpu, - render: (row: SpecsDetail) => { - const isActive = specs?.id === row.specs.id - const className = `${isActive ? 'text-main0' : ''} tp-body2` - return {row.specs.cpu} x86 64bit - }, + const columns = useMemo(() => { + const cols = [ + { + label: 'Cores', + sortable: true, + sortBy: (row: SpecsDetail) => row.specs.cpu, + render: (row: SpecsDetail) => ( + {row.specs.cpu} x86 64bit + ), + }, + { + label: 'Memory', + align: 'right', + sortable: true, + sortBy: (row: SpecsDetail) => row.ram, + render: (row: SpecsDetail) => ( + {row.ram} + ), + }, + { + label: 'Hold', + align: 'right', + sortable: true, + sortBy: (row: SpecsDetail) => row.price, + render: (row: SpecsDetail) => ( + {row.price} + ), + }, + { + label: '', + align: 'right', + render: (row: SpecsDetail) => { + return ( + + ) }, - { - label: 'Memory', - align: 'right', - sortable: true, - sortBy: (row: SpecsDetail) => row.ram, - render: (row: SpecsDetail) => { - const isActive = specs?.id === row.specs.id - const className = `${isActive ? 'text-main0' : ''}` - return {row.ram} - }, - }, - { - label: 'Hold', - align: 'right', - sortable: true, - sortBy: (row: SpecsDetail) => row.price, - render: (row: SpecsDetail) => { - const isActive = specs?.id === row.specs.id - const className = `${isActive ? 'text-main0' : ''}` - return {row.price} - }, - }, - { - label: '', - align: 'right', - render: (row: SpecsDetail) => { - const active = specs?.id === row.specs.id + }, + ] as TableColumn[] - return ( - - ) - }, + if (type === EntityType.Instance) { + cols.splice(2, 0, { + label: 'Storage', + align: 'right', + sortable: true, + sortBy: (row: SpecsDetail) => row.storage, + render: (row: SpecsDetail) => { + return {row.storage} }, - ] as TableColumn[] + }) + } - if (type === EntityType.Instance) { - cols.splice(2, 0, { - label: 'Storage', - align: 'right', - sortable: true, - sortBy: (row: SpecsDetail) => row.storage, - render: (row: SpecsDetail) => { - const isActive = specs?.id === row.specs.id - const className = `${isActive ? 'text-main0' : ''}` - return {row.storage} - }, - }) - } + return cols + }, [type]) - return cols - }, [specs, type]) + const data: SpecsDetail[] = useMemo(() => { + return options.map((specs) => { + const { ram, storage } = specs + const price = Executable.getExecutableCost({ + type, + specs, + isPersistent, + }) - const data: SpecsDetail[] = useMemo(() => { - return options.map((specs) => { - const { ram, storage } = specs - const price = Executable.getExecutableCost({ - type, - specs, - isPersistent, - }) + const isActive = specsCtrl.field.value.cpu === specs.cpu + const className = `${isActive ? 'text-main0' : ''}` - return { - specs, - storage: convertBitUnits(storage, { - from: 'mb', - to: 'gb', - displayUnit: true, - }) as string, - ram: convertBitUnits(ram, { - from: 'mb', - to: 'gb', - displayUnit: true, - }) as string, - price: price.computeTotalCost + ' ALEPH', - } - }) - }, [type, isPersistent, options]) + return { + specs, + isActive, + className, + storage: convertBitUnits(storage, { + from: 'mb', + to: 'gb', + displayUnit: true, + }) as string, + ram: convertBitUnits(ram, { + from: 'mb', + to: 'gb', + displayUnit: true, + }) as string, + price: price.computeTotalCost + ' ALEPH', + } + }) + }, [options, type, isPersistent, specsCtrl.field.value]) + + const getRowKey = useCallback((row: SpecsDetail) => row.specs.cpu + '', []) - const handleRowKey = useCallback((row: SpecsDetail) => row.specs.id, []) + const { onChange, ref } = specsCtrl.field - const handleRowProps = useCallback( - (row: SpecsDetail) => ({ onClick: () => handleChange(row.specs) }), - [handleChange], - ) + const handleRowProps = useCallback( + (row: SpecsDetail, rowIndex: number) => ({ + onClick: () => onChange(row.specs), + onKeyDown: (e: KeyboardEvent) => { + if (e.code !== 'Space' && e.code !== 'Enter') return + e.preventDefault() + onChange(row.specs) + }, + tabIndex: 0, + ref: rowIndex === 0 ? ref : undefined, + }), + [onChange, ref], + ) - return ( -
- -
- ) - }, -) + return ( +
+ + {specsCtrl.fieldState.error && ( + + )} +
+ ) +}) SelectInstanceSpecs.displayName = 'SelectInstanceSpecs' export default SelectInstanceSpecs diff --git a/src/components/form/SelectInstanceSpecs/types.ts b/src/components/form/SelectInstanceSpecs/types.ts index d70bcd91..c2a7c628 100644 --- a/src/components/form/SelectInstanceSpecs/types.ts +++ b/src/components/form/SelectInstanceSpecs/types.ts @@ -1,17 +1,20 @@ +import { Control } from 'react-hook-form' import { EntityType } from '@/helpers/constants' -import { InstanceSpecsProp } from '@/hooks/form/useSelectInstanceSpecs' +import { InstanceSpecsField } from '@/hooks/form/useSelectInstanceSpecs' export type SelectInstanceSpecsProps = { + name?: string + control: Control + options?: InstanceSpecsField[] type: EntityType.Instance | EntityType.Program - specs?: InstanceSpecsProp - options?: InstanceSpecsProp[] isPersistent?: boolean - onChange: (specs: InstanceSpecsProp) => void } export type SpecsDetail = { - specs: InstanceSpecsProp + specs: InstanceSpecsField storage: string // in GB ram: string // in GB price: string // in ALEPH + isActive: boolean + className: string } diff --git a/src/components/pages/dashboard/NewDomainPage/cmp.tsx b/src/components/pages/dashboard/NewDomainPage/cmp.tsx index 635c0567..b2ad1ce9 100644 --- a/src/components/pages/dashboard/NewDomainPage/cmp.tsx +++ b/src/components/pages/dashboard/NewDomainPage/cmp.tsx @@ -1,13 +1,15 @@ import ButtonLink from '@/components/common/ButtonLink' import Container from '@/components/common/CenteredContainer' +import CompositeTitle from '@/components/common/CompositeTitle' +import ExternalLinkButton from '@/components/common/ExternalLinkButton' import NoisyContainer from '@/components/common/NoisyContainer' +import Form from '@/components/form/Form' import { EntityType, EntityTypeName } from '@/helpers/constants' import { useNewDomainPage } from '@/hooks/pages/dashboard/useNewDomainPage' import { Button, Dropdown, DropdownOption, - Icon, Radio, RadioGroup, TextInput, @@ -15,86 +17,101 @@ import { export default function NewDomain() { const { - name, - ref, - entityType, entities, hasEntities, hasFunctions, hasInstances, - handleChangeName, - handleChangeEntityType, - handleChangeRef, + nameCtrl, + programTypeCtrl, + refCtrl, + errors, handleSubmit, } = useNewDomainPage() return ( <> -
-
- - {!hasEntities ? ( - <> -

- A domain should be linked to an existing resource. Try to - create an instance or function first + + {!hasEntities ? ( +

+ +

+ A domain should be linked to an existing resource. Try to create + an instance or function first +

+
+ + Create your first instance + +
+
+
+ ) : ( + <> +
+ + + Custom domain + +

+ Assign a user-friendly domain to your instance or function to + not only simplify access to your web3 application but also + enhance its professional appearance. This is an effective way + to elevate user experience, establish brand identity or + streamline the navigation process within your application.

-
- - Create your first instance - + + + +
+ + Learn more +
- - ) : ( - <> + +
+
+ + + Select Resource + +

+ You'll need to specify the resource your custom domain + will be associated with. This could either be an instance or a + function, depending on what you want your custom domain to + point to. +

-
- + + -
-
- - - - -
+ {entities.length > 0 && ( -
+
{entities.map(({ label, value }) => ( - {label} ))} @@ -110,14 +127,14 @@ export default function NewDomain() { size="regular" variant="primary" > - Save domain + Create domain
- - )} - -
- +
+
+ + )} + ) } diff --git a/src/components/pages/dashboard/NewFunctionPage/cmp.tsx b/src/components/pages/dashboard/NewFunctionPage/cmp.tsx index bfa77e7e..b55b0daf 100644 --- a/src/components/pages/dashboard/NewFunctionPage/cmp.tsx +++ b/src/components/pages/dashboard/NewFunctionPage/cmp.tsx @@ -1,398 +1,215 @@ -import { useState } from 'react' -import { - Button, - CodeEditor, - Icon, - Radio, - RadioGroup, - Tabs, - TextGradient, -} from '@aleph-front/aleph-core' +import { Button, Tabs } from '@aleph-front/aleph-core' +import { EntityType } from '@/helpers/constants' import CompositeTitle from '@/components/common/CompositeTitle' -import NoisyContainer from '@/components/common/NoisyContainer' -import HiddenFileInput from '@/components/common/HiddenFileInput' import { useNewFunctionPage } from '@/hooks/pages/dashboard/useNewFunctionPage' import HoldingRequirements from '@/components/common/HoldingRequirements' -import ExternalLinkButton from '@/components/common/ExternalLinkButton' -import InfoTooltipButton from '@/components/common/InfoTooltipButton' import SelectInstanceSpecs from '@/components/form/SelectInstanceSpecs' import AddVolumes from '@/components/form/AddVolumes' import AddEnvVars from '@/components/form/AddEnvVars' import AddDomains from '@/components/form/AddDomains' import AddNameAndTags from '@/components/form/AddNameAndTags' import SelectFunctionRuntime from '@/components/form/SelectFunctionRuntime' -import { EntityType } from '@/helpers/constants' import Container from '@/components/common/CenteredContainer' +import AddFunctionCode from '@/components/form/AddFunctionCode' +import SelectFunctionPersistence from '@/components/form/SelectFunctionPersistence' import BorderBox from '@/components/common/BorderBox' import { convertBitUnits } from '@/helpers/utils' +import Form from '@/components/form/Form' export default function NewFunctionPage() { const { - formState, - handleSubmit, - setFormValue, address, accountBalance, isCreateButtonDisabled, + values, + control, + errors, + handleSubmit, handleChangeEntityTab, - handleChangeFunctionRuntime, - handleChangeInstanceSpecs, - handleChangeVolumes, - handleChangeEnvVars, - handleChangeDomains, - handleChangeNameAndTags, } = useNewFunctionPage() - const [tabId, setTabId] = useState('code') - return ( - <> -
-
- - - -
-
- - - Code to execute - -

- If your code has any dependencies, you can upload them separately - in the volume section below to ensure a faster creation. -

-
- { - setTabId(id) - setFormValue('codeOrFile', id) - }} - /> -
-
- {tabId === 'code' ? ( - <> -

- To get started you can start adding your code in the window - below. -

-
- - - - setFormValue('codeLanguage', 'python') - } - value="on-demand" - /> - - setFormValue('codeLanguage', 'javascript') - } - value="persistent" - /> - - -
-
- setFormValue('functionCode', value)} - value={formState.functionCode} - defaultLanguage={formState.codeLanguage} - language={formState.codeLanguage} - /> -
-
- -
-
Write code
-
- Your code should have an app function that will - serve as an entrypoint to the program. -
-
-
-
Upload code
-
- Your zip file should contain a main file (ex: - main.py) at its root that exposes an app function. - This will serve as an entrypoint to the program. -
-
-
- } - > - Learn more - -
- - ) : tabId === 'file' ? ( - <> -

- To get started, compress your code into a zip file and - upload it here. -

-

- Your zip archive should contain a{' '} - main file (ex: main.py) at - its root that exposes an app{' '} - function. This will serve as an entrypoint to the program -

- -
- setFormValue('functionFile', file)} - > - Upload code - -
-
- - ) : ( - <> - )} -
- - -
- - - Select runtime - -

- Select the optimal environment for executing your functions, - tailored to your specific requirements. Below are the available - options -

- -
-
-
- - - Type of scheduling - -

- Configure if this program should be running continuously, - persistent, or only on-demand in response to a user request or an - event. -

- - - setFormValue('isPersistent', true)} - value="persistent" - /> - setFormValue('isPersistent', false)} - value="on-demand" - /> - - -
- - Learn more - -
-
-
-
- - - Select an instance size - -

- Select the hardware resources allocated to your functions, - ensuring optimal performance and efficient resource usage tailored - to your specific needs. -

- -
-
-
- - - Name and tags - -

- Organize and identify your functions more effectively by assigning - a unique name, obtaining a hash reference, and defining multiple - tags. This helps streamline your development process and makes it - easier to manage your web3 functions. -

- -
-
-
- - - Add volumes - - {formState.specs && ( - - Good news! Your selected package already includes{' '} - - {convertBitUnits(formState.specs.storage, { - from: 'mb', - to: 'gb', - displayUnit: true, - })} - {' '} - of storage at no additional cost. This has been factored into - your configuration to maximize efficiency and value. Feel free - to adjust as necessary. - - )} - - -
-
- - - Add environment variables - -

- Define key-value pairs that act as configuration settings for your - web3 function. Environment variables offer a convenient way to - store information, manage configurations, and modify your - application's behaviour without altering the source code. -

- -
-
-
- - - Custom domain - -

- Configure a user-friendly domain name for your web3 function, - providing a more accessible and professional way for users to - interact with your application. -

- -
-
-
- - - Estimated holding requirements - -
-

- This amount needs to be present in your wallet until the - function is removed. Tokens won’t be locked nor consumed. The - function will be garbage collected once funds are removed from - the wallet. -

-
-
- -
-
- -
-
-
- - +
+
+ + + +
+
+ + + Code to execute + +

+ If your code has any dependencies, you can upload them separately in + the volume section below to ensure a faster creation. +

+ +
+
+
+ + + Select runtime + +

+ Select the optimal environment for executing your functions, + tailored to your specific requirements. Below are the available + options +

+ +
+
+
+ + + Type of scheduling + +

+ Configure if this program should be running continuously, + persistent, or only on-demand in response to a user request or an + event. +

+ +
+
+
+ + + Select an instance size + +

+ Select the hardware resources allocated to your functions, ensuring + optimal performance and efficient resource usage tailored to your + specific needs. +

+ +
+
+
+ + + Name and tags + +

+ Organize and identify your functions more effectively by assigning a + unique name, obtaining a hash reference, and defining multiple tags. + This helps streamline your development process and makes it easier + to manage your web3 functions. +

+ +
+
+
+ + + Add volumes + + {values.specs && ( + + Good news! Your selected package already includes{' '} + + {convertBitUnits(values.specs.storage, { + from: 'mb', + to: 'gb', + displayUnit: true, + })} + {' '} + of storage at no additional cost. This has been factored into your + configuration to maximize efficiency and value. Feel free to + adjust as necessary. + + )} + + +
+
+ + + Add environment variables + +

+ Define key-value pairs that act as configuration settings for your + web3 function. Environment variables offer a convenient way to store + information, manage configurations, and modify your + application's behaviour without altering the source code. +

+ +
+
+
+ + + Custom domain + +

+ Configure a user-friendly domain name for your web3 function, + providing a more accessible and professional way for users to + interact with your application. +

+ +
+
+ + This amount needs to be present in your wallet until the function is + removed. Tokens won't be locked nor consumed. The function will + be garbage collected once funds are removed from the wallet. + + } + button={ + + } + /> + ) } diff --git a/src/components/pages/dashboard/NewInstancePage/cmp.tsx b/src/components/pages/dashboard/NewInstancePage/cmp.tsx index 57ccf227..ceed25dd 100644 --- a/src/components/pages/dashboard/NewInstancePage/cmp.tsx +++ b/src/components/pages/dashboard/NewInstancePage/cmp.tsx @@ -1,4 +1,4 @@ -import { Button, Tabs, TextGradient } from '@aleph-front/aleph-core' +import { Button, Tabs } from '@aleph-front/aleph-core' import CompositeTitle from '@/components/common/CompositeTitle' import SelectInstanceImage from '@/components/form/SelectInstanceImage' import SelectInstanceSpecs from '@/components/form/SelectInstanceSpecs' @@ -11,226 +11,186 @@ import HoldingRequirements from '@/components/common/HoldingRequirements' import { EntityType } from '@/helpers/constants' import Container from '@/components/common/CenteredContainer' import { useNewInstancePage } from '@/hooks/pages/dashboard/useNewInstancePage' +import Form from '@/components/form/Form' export default function NewInstancePage() { const { - formState, address, accountBalance, isCreateButtonDisabled, + values, + control, + errors, handleSubmit, handleChangeEntityTab, - handleChangeInstanceImage, - handleChangeInstanceSpecs, - handleChangeVolumes, - handleChangeEnvVars, - handleChangeSSHKeys, - handleChangeDomains, - handleChangeNameAndTags, } = useNewInstancePage() return ( - <> -
-
- - +
+ + + +
+
+ + + Choose an image + +

+ Chose a base image for your VM. It’s the base system that you will + be able to customize. +

+
+ +
+
+
+
+ + + Select an instance size + +

+ Please select one of the available instance size as a base for your + VM. You will be able to customize the volumes later. +

+
+ - -
-
- - - Choose an image - -

- Chose a base image for your VM. It’s the base system that you will - be able to customize. -

-
- -
- {/*
- - Learn more - -
*/} -
-
-
- - - Select an instance size - -

- Please select one of the available instance size as a base for - your VM. You will be able to customize the volumes later. -

-
- -
-
-
-
- - - Add volumes - -
- -
-
-
-
- - - Configure SSH Key - -

- Access your cloud instances securely. Give existing key’s below - access to this instance or add new keys. Remember, storing private - keys safely is crucial for security. If you need help, our support - team is always ready to assist. -

-
- -
-
-
-
- - - Add environment variables - -

- Define key-value pairs that act as configuration settings for your - web3 instance. Environment variables offer a convenient way to - store information, manage configurations, and modify your - application's behaviour without altering the source code. -

-
- -
-
-
-
- - - Custom domain - -

- You have the ability to configure a domain name to access your - cloud instances. By setting up a user-friendly custom domain, - accessing your instances becomes easier and more intuitive. - It&s another way we&re making web3 cloud management as - straightforward as possible. -

- -
-
-
- - - Name and tags - -

- Organize and identify your instances more effectively by assigning - a unique name, obtaining a hash reference, and defining multiple - tags. This helps streamline your development process and makes it - easier to manage your web3 instances. -

- -
-
-
- - - Estimated holding requirements - -
-

- This amount needs to be present in your wallet until the - instance is removed. Tokens won’t be locked nor consumed. The - instance will be garbage collected once funds are removed from - the wallet. -

-
-
- -
-
- -
-
-
- - +
+ + +
+ + + Add volumes + +
+ +
+
+
+
+ + + Configure SSH Key + +

+ Access your cloud instances securely. Give existing key’s below + access to this instance or add new keys. Remember, storing private + keys safely is crucial for security. If you need help, our support + team is always ready to assist. +

+
+ +
+
+
+
+ + + Add environment variables + +

+ Define key-value pairs that act as configuration settings for your + web3 instance. Environment variables offer a convenient way to store + information, manage configurations, and modify your + application's behaviour without altering the source code. +

+
+ +
+
+
+
+ + + Custom domain + +

+ You have the ability to configure a domain name to access your cloud + instances. By setting up a user-friendly custom domain, accessing + your instances becomes easier and more intuitive. It&s another + way we&re making web3 cloud management as straightforward as + possible. +

+ +
+
+
+ + + Name and tags + +

+ Organize and identify your instances more effectively by assigning a + unique name, obtaining a hash reference, and defining multiple tags. + This helps streamline your development process and makes it easier + to manage your web3 instances. +

+ +
+
+ + This amount needs to be present in your wallet until the instance is + removed. Tokens won't be locked nor consumed. The instance will + be garbage collected once funds are removed from the wallet. + + } + button={ + + } + /> + ) } diff --git a/src/components/pages/dashboard/NewSSHKeyPage/cmp.tsx b/src/components/pages/dashboard/NewSSHKeyPage/cmp.tsx index 851cd120..99e0f27a 100644 --- a/src/components/pages/dashboard/NewSSHKeyPage/cmp.tsx +++ b/src/components/pages/dashboard/NewSSHKeyPage/cmp.tsx @@ -1,52 +1,58 @@ import Container from '@/components/common/CenteredContainer' +import CompositeTitle from '@/components/common/CompositeTitle' import NoisyContainer from '@/components/common/NoisyContainer' +import Form from '@/components/form/Form' import { useNewSSHKeyPage } from '@/hooks/pages/dashboard/useNewSSHKeyPage' import { Button, TextArea, TextInput } from '@aleph-front/aleph-core' export default function NewSSHKey() { - const { key, label, handleChangeKey, handleChangeLabel, handleSubmit } = - useNewSSHKeyPage() + const { keyCtrl, labelCtrl, handleSubmit, errors } = useNewSSHKeyPage() return ( - <> -
-
- - -
-