diff --git a/components/autocomplete/index.tsx b/components/autocomplete/index.tsx index e289cbe0..7f317707 100644 --- a/components/autocomplete/index.tsx +++ b/components/autocomplete/index.tsx @@ -1,4 +1,4 @@ -import React, { ForwardedRef, FunctionComponent } from 'react'; +import React, { ForwardedRef, FunctionComponent, useMemo } from 'react'; import { Autocomplete as AutocompleteMUI, CircularProgress, SxProps } from '@mui/material'; import { ControllerRenderProps, FieldValues } from 'react-hook-form'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; @@ -35,6 +35,9 @@ const AutocompleteComponent: FunctionComponent = ({ disabled, ...props }) => { + // Unfortunately MaterialUI doesn't not update the dom state when no options are available + const value = useMemo(() => (options.length ? props.value : ''), [options.length, props.value]); + return ( = ({ }, }} {...props} + isOptionEqualToValue={(option: string, value: string) => option === value} renderInput={(params) => ( = ({ )} } + value={value} {...params} label={label} /> diff --git a/components/clusterDetails/clusterDetails.stories.tsx b/components/clusterDetails/clusterDetails.stories.tsx index f4593dc3..8594a3f3 100644 --- a/components/clusterDetails/clusterDetails.stories.tsx +++ b/components/clusterDetails/clusterDetails.stories.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Story } from '@storybook/react'; -import ClusterDetails from '.'; - import { ClusterInfo } from '../clusterTable/clusterTable'; import { ClusterStatus, ClusterType } from '../../types/provision'; import { InstallationType } from '../../types/redux'; +import ClusterDetails from '.'; + export default { title: 'Components/ClusterDetails', component: ClusterDetails, diff --git a/components/controlledFields/AutoComplete.tsx b/components/controlledFields/AutoComplete.tsx index 03af5f0c..970455ca 100644 --- a/components/controlledFields/AutoComplete.tsx +++ b/components/controlledFields/AutoComplete.tsx @@ -48,6 +48,7 @@ function ControlledAutocomplete({ field.onBlur(); setIsBlur(true); }} + value={options.length ? field.value : ''} loading={loading} placeholder={placeholder} disabled={disabled} diff --git a/components/controlledFields/Radio.tsx b/components/controlledFields/Radio.tsx new file mode 100644 index 00000000..acee1978 --- /dev/null +++ b/components/controlledFields/Radio.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { FormControlLabel, FormGroup, Radio, RadioGroup } from '@mui/material'; +import { Control, Controller, UseControllerProps, FieldValues } from 'react-hook-form'; + +import { VOLCANIC_SAND } from '../../constants/colors'; +import Typography from '../typography'; + +export interface ControlledTextFieldProps extends UseControllerProps { + control: Control; + required?: boolean; + rules: { + required: boolean; + pattern?: RegExp; + }; + options: Array<{ label: string; value: string }>; +} + +function ControlledRadio({ + defaultValue, + name, + options, + required, + ...props +}: ControlledTextFieldProps) { + return ( + ( + + + {options.map(({ label, value }) => ( + { + field.onChange(e); + }} + /> + } + label={ + + {label} + + } + /> + ))} + + + )} + /> + ); +} + +export default ControlledRadio; diff --git a/components/flow/index.tsx b/components/flow/index.tsx index 00d79e49..3437eb8d 100644 --- a/components/flow/index.tsx +++ b/components/flow/index.tsx @@ -87,7 +87,7 @@ const nodeTypes: NodeTypes = { }; export const Flow: FunctionComponent = () => { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [nodes, , onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = useCallback( (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), diff --git a/containers/clusterForms/aws/index.tsx b/containers/clusterForms/aws/index.tsx index 52c39312..6878ce32 100644 --- a/containers/clusterForms/aws/index.tsx +++ b/containers/clusterForms/aws/index.tsx @@ -4,12 +4,11 @@ import TerminalLogs from '../../terminalLogs'; import { FormStep } from '../../../constants/installation'; import AuthForm from '../shared/authForm'; import ClusterRunning from '../shared/clusterRunning'; - -import AwsSetupForm from './setupForm'; +import SetupForm from '../shared/setupForm'; const AWS_FORM_FLOW = { [FormStep.AUTHENTICATION]: AuthForm, - [FormStep.SETUP]: AwsSetupForm, + [FormStep.SETUP]: SetupForm, [FormStep.PROVISIONING]: TerminalLogs, [FormStep.READY]: ClusterRunning, }; diff --git a/containers/clusterForms/aws/setupForm/index.tsx b/containers/clusterForms/aws/setupForm/index.tsx deleted file mode 100644 index 49f0950e..00000000 --- a/containers/clusterForms/aws/setupForm/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; -import ControlledTextField from '../../../../components/controlledFields/TextField'; -// import LearnMore from '../../../../components/learnMore'; -import { useAppDispatch, useAppSelector } from '../../../../redux/store'; -import { getCloudDomains } from '../../../../redux/thunks/api.thunk'; -import { InstallValues } from '../../../../types/redux'; -import { EMAIL_REGEX } from '../../../../constants/index'; - -const AwsSetupForm: FunctionComponent = () => { - const dispatch = useAppDispatch(); - const { cloudDomains, cloudRegions, values } = useAppSelector(({ api, installation }) => ({ - cloudDomains: api.cloudDomains, - cloudRegions: api.cloudRegions, - values: installation.values, - })); - - const handleRegionOnSelect = async (region: string) => { - dispatch(getCloudDomains(region)); - }; - - const formatDomains = (domains: Array) => { - return domains.map((domain) => { - const formattedDomain = domain[domain.length - 1].includes('.') - ? domain.substring(0, domain.length - 1) - : domain; - return { label: formattedDomain, value: formattedDomain }; - }); - }; - - const { control } = useFormContext(); - - return ( - <> - - ({ label: region, value: region }))} - onChange={handleRegionOnSelect} - /> - - - {/* */} - - ); -}; - -export default AwsSetupForm; diff --git a/containers/clusterForms/civo/index.tsx b/containers/clusterForms/civo/index.tsx index df1a3821..16754864 100644 --- a/containers/clusterForms/civo/index.tsx +++ b/containers/clusterForms/civo/index.tsx @@ -4,8 +4,7 @@ import TerminalLogs from '../../terminalLogs'; import { FormStep } from '../../../constants/installation'; import AuthForm from '../shared/authForm'; import ClusterRunning from '../shared/clusterRunning'; - -import SetupForm from './setupForm'; +import SetupForm from '../shared/setupForm'; const CIVO_FORM_FLOW = { [FormStep.AUTHENTICATION]: AuthForm, diff --git a/containers/clusterForms/civo/setupForm/index.tsx b/containers/clusterForms/civo/setupForm/index.tsx deleted file mode 100644 index f0f203e0..00000000 --- a/containers/clusterForms/civo/setupForm/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import { useFormContext } from 'react-hook-form'; - -import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; -import ControlledTextField from '../../../../components/controlledFields/TextField'; -// import LearnMore from '../../../../components/learnMore'; -import { useAppDispatch, useAppSelector } from '../../../../redux/store'; -import { getCloudDomains } from '../../../../redux/thunks/api.thunk'; -import { InstallValues } from '../../../../types/redux'; -import { EMAIL_REGEX } from '../../../../constants'; - -const CivoSetupForm: FunctionComponent = () => { - const dispatch = useAppDispatch(); - const { cloudDomains, cloudRegions, values } = useAppSelector(({ api, installation }) => ({ - cloudDomains: api.cloudDomains, - cloudRegions: api.cloudRegions, - values: installation.values, - })); - - const { control } = useFormContext(); - - const handleRegionOnSelect = async (region: string) => { - dispatch(getCloudDomains(region)); - }; - - return ( - <> - - ({ label: region, value: region }))} - onChange={handleRegionOnSelect} - /> - ({ label: domain, value: domain }))} - /> - - {/* */} - - ); -}; - -export default CivoSetupForm; diff --git a/containers/clusterForms/clusterCreation/clusterCreation.stories.tsx b/containers/clusterForms/clusterCreation/clusterCreation.stories.tsx index fa3610b6..4eef21f5 100644 --- a/containers/clusterForms/clusterCreation/clusterCreation.stories.tsx +++ b/containers/clusterForms/clusterCreation/clusterCreation.stories.tsx @@ -1,5 +1,6 @@ import React, { useRef } from 'react'; import { Story } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; import { FormProvider, useForm } from 'react-hook-form'; import ClusterCreationForm, { ClusterConfig } from '.'; @@ -22,10 +23,7 @@ const DefaultTemplate: Story = () => { return ( - console.log('the form values =>', config)} - /> + action('onSubmit')} /> ); diff --git a/containers/clusterForms/clusterCreation/index.tsx b/containers/clusterForms/clusterCreation/index.tsx index 8a056177..00db3c8c 100644 --- a/containers/clusterForms/clusterCreation/index.tsx +++ b/containers/clusterForms/clusterCreation/index.tsx @@ -9,6 +9,7 @@ import ControlledSelect from '../../../components/controlledFields/Select'; import NumberInput from '../../../components/numberInput'; import Row from '../../../components/row'; import Button from '../../../components/button'; + import { Form } from './clusterCreation.styled'; export type ClusterConfig = { diff --git a/containers/clusterForms/digitalocean/index.tsx b/containers/clusterForms/digitalocean/index.tsx index 3e4398c6..88b93252 100644 --- a/containers/clusterForms/digitalocean/index.tsx +++ b/containers/clusterForms/digitalocean/index.tsx @@ -4,12 +4,11 @@ import TerminalLogs from '../../terminalLogs'; import { FormStep } from '../../../constants/installation'; import AuthForm from '../shared/authForm'; import ClusterRunning from '../shared/clusterRunning'; - -import DigitalOceanSetupForm from './setupForm'; +import SetupForm from '../shared/setupForm'; const DIGITAL_OCEAN_FORM_FLOW = { [FormStep.AUTHENTICATION]: AuthForm, - [FormStep.SETUP]: DigitalOceanSetupForm, + [FormStep.SETUP]: SetupForm, [FormStep.PROVISIONING]: TerminalLogs, [FormStep.READY]: ClusterRunning, }; diff --git a/containers/clusterForms/digitalocean/setupForm/index.tsx b/containers/clusterForms/digitalocean/setupForm/index.tsx deleted file mode 100644 index 69805ad6..00000000 --- a/containers/clusterForms/digitalocean/setupForm/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import { useFormContext } from 'react-hook-form'; - -// import LearnMore from '../../../../components/learnMore'; -import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; -import ControlledTextField from '../../../../components/controlledFields/TextField'; -import { useAppDispatch, useAppSelector } from '../../../../redux/store'; -import { getCloudDomains } from '../../../../redux/thunks/api.thunk'; -import { EMAIL_REGEX } from '../../../../constants'; -import { InstallValues } from '../../../../types/redux'; - -const DigitalOceanSetupForm: FunctionComponent = () => { - const dispatch = useAppDispatch(); - const { cloudDomains, cloudRegions, values } = useAppSelector(({ api, installation }) => ({ - cloudDomains: api.cloudDomains, - cloudRegions: api.cloudRegions, - values: installation.values, - })); - - const { control } = useFormContext(); - - const handleRegionOnSelect = async (region: string) => { - dispatch(getCloudDomains(region)); - }; - - return ( - <> - - ({ label: region, value: region }))} - onChange={handleRegionOnSelect} - /> - ({ label: domain, value: domain }))} - /> - - {/* */} - - ); -}; - -export default DigitalOceanSetupForm; diff --git a/containers/clusterForms/shared/advancedOptions/index.tsx b/containers/clusterForms/shared/advancedOptions/index.tsx index 2976d0ad..201a5572 100644 --- a/containers/clusterForms/shared/advancedOptions/index.tsx +++ b/containers/clusterForms/shared/advancedOptions/index.tsx @@ -1,29 +1,34 @@ -import React, { ChangeEvent, FunctionComponent, useState } from 'react'; +import React, { ChangeEvent, FunctionComponent, useMemo, useState } from 'react'; import { useFormContext } from 'react-hook-form'; +import { Required } from '../../../../components/textField/textField.styled'; import LearnMore from '../../../../components/learnMore'; import Typography from '../../../../components/typography'; import SwitchComponent from '../../../../components/switch'; import Checkbox from '../../../../components/controlledFields/Checkbox'; import ControlledTextField from '../../../../components/controlledFields/TextField'; -import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; +import ControlledRadio from '../../../../components/controlledFields/Radio'; import { useAppSelector } from '../../../../redux/store'; -import { InstallValues } from '../../../../types/redux'; +import { InstallValues, InstallationType } from '../../../../types/redux'; +import { GitProvider } from '../../../../types'; import { EXCLUSIVE_PLUM } from '../../../../constants/colors'; import { CheckboxContainer, Switch } from './advancedOptions.styled'; const AdvancedOptions: FunctionComponent = () => { const [isAdvancedOptionsEnabled, setIsAdvancedOptionsEnabled] = useState(false); - const [isCloudFlareSelected, setIsCloudFlareSelected] = useState(false); const handleOnChangeSwitch = ({ target }: ChangeEvent) => { setIsAdvancedOptionsEnabled(target.checked); }; - const { values, installType } = useAppSelector(({ installation }) => installation); + const { values, installType, gitProvider } = useAppSelector(({ installation }) => installation); + + const isGitHub = useMemo(() => gitProvider === GitProvider.GITHUB, [gitProvider]); + const gitLabel = useMemo(() => (isGitHub ? 'GitHub' : 'GitLab'), [isGitHub]); const { control } = useFormContext(); + const isAwsInstallation = useMemo(() => installType === InstallationType.AWS, [installType]); return ( <> @@ -65,31 +70,27 @@ const AdvancedOptions: FunctionComponent = () => { }} /> - setIsCloudFlareSelected(value === 'cloudflare')} - rules={{ - required: false, - }} - /> - {isCloudFlareSelected && ( - + {isAwsInstallation && ( + + + Manage image repositories with * + + + )} { await dispatch(getGitHubOrgTeams({ token, organization: gitOwner })).unwrap(); } else { await dispatch(getGitLabProjects({ token, group: gitOwner })); + await dispatch(getGitLabSubgroups({ token, group: gitOwner })); } } }; diff --git a/containers/clusterForms/shared/dnsProvider/index.tsx b/containers/clusterForms/shared/dnsProvider/index.tsx new file mode 100644 index 00000000..fe3247fa --- /dev/null +++ b/containers/clusterForms/shared/dnsProvider/index.tsx @@ -0,0 +1,79 @@ +import React, { ChangeEvent, FunctionComponent, useState } from 'react'; +import { Control, UseFormReset, UseFormSetValue } from 'react-hook-form'; +import { getCloudDomains } from 'redux/thunks/api.thunk'; + +import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; +import { clearDomains } from '../../../../redux/slices/api.slice'; +import { useAppDispatch, useAppSelector } from '../../../../redux/store'; +import { InstallValues } from '../../../../types/redux'; +import ControlledPassword from '../../../../components/controlledFields/Password'; + +export interface DnsProviderProps { + control: Control; + reset?: UseFormReset; + selectedRegion: string; + setValue: UseFormSetValue; +} + +const DnsProvider: FunctionComponent = ({ control, reset, selectedRegion }) => { + const dispatch = useAppDispatch(); + const [isCloudFlareSelected, setIsCloudFlareSelected] = useState(false); + + const { installType } = useAppSelector(({ api, installation }) => ({ + cloudDomains: api.cloudDomains, + cloudRegions: api.cloudRegions, + values: installation.values, + installType: installation.installType, + })); + + const handleCloudfareToken = ({ + target, + }: ChangeEvent) => { + const { value } = target; + + dispatch(getCloudDomains({ region: selectedRegion, cloudflareToken: value })); + }; + + const handleDnsProviderOnChange = (value: string) => { + const isCloudflare = value === 'cloudflare'; + setIsCloudFlareSelected(isCloudflare); + reset && reset({ cloudflareToken: '' }); + + if (!isCloudflare) { + dispatch(getCloudDomains({ region: selectedRegion })); + } else { + dispatch(clearDomains()); + } + }; + + return ( + <> + + {isCloudFlareSelected && ( + + )} + + ); +}; + +export default DnsProvider; diff --git a/containers/clusterForms/shared/setupForm/index.tsx b/containers/clusterForms/shared/setupForm/index.tsx new file mode 100644 index 00000000..5c9d9a39 --- /dev/null +++ b/containers/clusterForms/shared/setupForm/index.tsx @@ -0,0 +1,158 @@ +import React, { ChangeEvent, FunctionComponent, useMemo, useState } from 'react'; + +import { clearDomains } from '../../../../redux/slices/api.slice'; +import ControlledPassword from '../../../../components/controlledFields/Password'; +import { useAppDispatch, useAppSelector } from '../../../../redux/store'; +import { getCloudDomains } from '../../../../redux/thunks/api.thunk'; +import ControlledTextField from '../../../../components/controlledFields/TextField'; +import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; +import { EMAIL_REGEX } from '../../../../constants'; +import { FormFlowProps } from '../../../../types/provision'; +import { InstallValues, InstallationType } from '../../../../types/redux'; + +export interface SetupFormProps { + children: FunctionComponent; +} + +const CLOUD_REGION_LABELS: Record = { + [InstallationType.AWS]: 'Cloud region', + [InstallationType.CIVO]: 'Cloud region', + [InstallationType.DIGITAL_OCEAN]: 'Datacenter region', + [InstallationType.VULTR]: 'Cloud location', + [InstallationType.LOCAL]: null, +}; + +const SetupForm: FunctionComponent> = ({ control, setValue }) => { + const [isCloudFlareSelected, setIsCloudFlareSelected] = useState(false); + const [selectedRegion, setSelectedRegion] = useState(''); + const dispatch = useAppDispatch(); + + const { cloudDomains, cloudRegions, installType, values } = useAppSelector( + ({ api, installation }) => ({ + cloudDomains: api.cloudDomains, + cloudRegions: api.cloudRegions, + values: installation.values, + installType: installation.installType, + }), + ); + + const cloudRegionLabel = useMemo( + () => + CLOUD_REGION_LABELS[installType as InstallationType] || + (CLOUD_REGION_LABELS[InstallationType.AWS] as string), + [installType], + ); + + const handleRegionOnSelect = async (region: string) => { + setSelectedRegion(region); + dispatch(getCloudDomains({ region })); + }; + + const formatDomains = (domains: Array) => { + return domains.map((domain) => { + const formattedDomain = + installType === InstallationType.AWS && domain[domain.length - 1].includes('.') + ? domain.substring(0, domain.length - 1) + : domain; + return { label: formattedDomain, value: formattedDomain }; + }); + }; + + const handleCloudfareToken = ({ + target, + }: ChangeEvent) => { + const { value } = target; + + dispatch(getCloudDomains({ region: selectedRegion, cloudflareToken: value })); + }; + + const handleDnsProviderOnChange = (value: string) => { + const isCloudflare = value === 'cloudflare'; + setIsCloudFlareSelected(isCloudflare); + + if (!isCloudflare) { + dispatch(getCloudDomains({ region: selectedRegion })); + setValue && setValue('cloudflareToken', ''); + } else { + dispatch(clearDomains()); + setValue && setValue('domainName', ''); + } + }; + + return ( + <> + + ({ label: region, value: region }))} + onChange={handleRegionOnSelect} + /> + + {isCloudFlareSelected && ( + + )} + + + {/* */} + + ); +}; + +export default SetupForm; diff --git a/containers/clusterForms/vultr/index.tsx b/containers/clusterForms/vultr/index.tsx index 3fc35dd6..bc91a9ab 100644 --- a/containers/clusterForms/vultr/index.tsx +++ b/containers/clusterForms/vultr/index.tsx @@ -4,12 +4,11 @@ import TerminalLogs from '../../terminalLogs'; import { FormStep } from '../../../constants/installation'; import AuthForm from '../shared/authForm'; import ClusterRunning from '../shared/clusterRunning'; - -import AwsSetupForm from './setupForm'; +import SetupForm from '../shared/setupForm'; const VULTR_FORM_FLOW = { [FormStep.AUTHENTICATION]: AuthForm, - [FormStep.SETUP]: AwsSetupForm, + [FormStep.SETUP]: SetupForm, [FormStep.PROVISIONING]: TerminalLogs, [FormStep.READY]: ClusterRunning, }; diff --git a/containers/clusterForms/vultr/setupForm/index.tsx b/containers/clusterForms/vultr/setupForm/index.tsx deleted file mode 100644 index a40cf153..00000000 --- a/containers/clusterForms/vultr/setupForm/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FunctionComponent } from 'react'; -import { useFormContext } from 'react-hook-form'; - -// import LearnMore from '../../../../components/learnMore'; -import ControlledTextField from '../../../../components/controlledFields/TextField'; -import ControlledAutocomplete from '../../../../components/controlledFields/AutoComplete'; -import { useAppDispatch, useAppSelector } from '../../../../redux/store'; -import { getCloudDomains } from '../../../../redux/thunks/api.thunk'; -import { InstallValues } from '../../../../types/redux'; -import { EMAIL_REGEX } from '../../../../constants'; - -const CivoSetupForm: FunctionComponent = () => { - const dispatch = useAppDispatch(); - const { cloudDomains, cloudRegions, values } = useAppSelector(({ api, installation }) => ({ - cloudDomains: api.cloudDomains, - cloudRegions: api.cloudRegions, - values: installation.values, - })); - - const { control } = useFormContext(); - - const handleRegionOnSelect = async (region: string) => { - dispatch(getCloudDomains(region)); - }; - - return ( - <> - - ({ label: region, value: region }))} - onChange={handleRegionOnSelect} - /> - ({ label: domain, value: domain }))} - /> - - {/* */} - - ); -}; - -export default CivoSetupForm; diff --git a/containers/provision/provision.styled.ts b/containers/provision/provision.styled.ts index 156c10a7..84e80245 100644 --- a/containers/provision/provision.styled.ts +++ b/containers/provision/provision.styled.ts @@ -1,4 +1,4 @@ -import { Box } from '@mui/material'; +import { Box, BoxProps } from '@mui/material'; import styled from 'styled-components'; import FormContainer from '../../components/formContainer'; @@ -15,7 +15,7 @@ export const AdvancedOptionsContainer = styled(FormContainer)` `}; `; -export const Form = styled(Box)` +export const Form = styled(Box)` height: 100%; overflow: auto; `; diff --git a/redux/slices/api.slice.ts b/redux/slices/api.slice.ts index b5b07609..195e92c2 100644 --- a/redux/slices/api.slice.ts +++ b/redux/slices/api.slice.ts @@ -55,6 +55,9 @@ const apiSlice = createSlice({ clearValidation: (state) => { state.isAuthenticationValid = undefined; }, + clearDomains: (state) => { + state.cloudDomains = []; + }, }, extraReducers: (builder) => { builder @@ -101,6 +104,11 @@ const apiSlice = createSlice({ state.isError = false; state.clusters = payload; }) + .addCase(getClusters.rejected, (state) => { + state.loading = false; + state.isError = true; + state.clusters = []; + }) .addCase(getCloudDomains.fulfilled, (state, { payload }: PayloadAction>) => { state.cloudDomains = payload; }) @@ -114,6 +122,7 @@ const apiSlice = createSlice({ }, }); -export const { clearValidation, setCompletedSteps, clearClusterState } = apiSlice.actions; +export const { clearValidation, setCompletedSteps, clearClusterState, clearDomains } = + apiSlice.actions; export const apiReducer = apiSlice.reducer; diff --git a/redux/slices/git.slice.ts b/redux/slices/git.slice.ts index 30dcb849..063407bf 100644 --- a/redux/slices/git.slice.ts +++ b/redux/slices/git.slice.ts @@ -8,6 +8,7 @@ import { getGitHubOrgTeams, getGitlabGroups, getGitlabUser, + getGitLabSubgroups, getGitLabProjects, } from '../thunks/git.thunk'; import { GitLabGroup, GitLabUser } from '../../types/gitlab'; @@ -147,22 +148,21 @@ const gitSlice = createSlice({ KUBEFIRST_REPOSITORIES.includes(name), ); - const kubefirstTeams = state.gitlabGroups.filter(({ name }) => - KUBEFIRST_TEAMS.includes(name), - ); - - if (kubefirstTeams.length) { + if (kubefirstRepos.length) { state.errors.push(` GitLab organization ${state.gitOwner} - already has teams named admins or developers. - Please remove or rename them to continue.`); + already has repositories named either gitops and metaphor. + Please remove or rename to continue.`); } + }) + .addCase(getGitLabSubgroups.fulfilled, (state, { payload: gitlabSubgroups }) => { + const kubefirstTeams = gitlabSubgroups.filter(({ name }) => KUBEFIRST_TEAMS.includes(name)); - if (kubefirstRepos.length) { + if (kubefirstTeams.length) { state.errors.push(` GitLab organization ${state.gitOwner} - already has repositories named either gitops and metaphor. - Please remove or rename to continue.`); + already has teams named admins or developers. + Please remove or rename them to continue.`); } }); }, diff --git a/redux/thunks/api.thunk.ts b/redux/thunks/api.thunk.ts index 76e7be62..9beee619 100644 --- a/redux/thunks/api.thunk.ts +++ b/redux/thunks/api.thunk.ts @@ -22,7 +22,7 @@ const mapClusterFromRaw = (cluster: ClusterResponse): Cluster => ({ cloudProvider: cluster.cloud_provider, cloudRegion: cluster.cloud_region, domainName: cluster.domain_name, - gitOwner: cluster.git_owner, + gitAuth: cluster.gitAuth, gitProvider: cluster.git_provider, gitUser: cluster.git_user, type: cluster.cluster_type, @@ -69,15 +69,20 @@ export const createCluster = createAsyncThunk< cloud_provider: installType?.toString(), cloud_region: values?.cloudRegion, domain_name: values?.domainName, - git_owner: values?.gitOwner, git_provider: gitProvider, - git_token: values?.gitToken, gitops_template_url: values?.gitopsTemplateUrl, gitops_template_branch: values?.gitopsTemplateBranch, git_protocol: values?.useHttps ? 'https' : 'ssh', dns_provider: values?.dnsProvider, - cloudflare_api_token: values?.cloudflareToken, + ecr: values?.imageRepository === 'ecr', type: 'mgmt', + git_auth: { + git_owner: values?.gitOwner, + git_token: values?.gitToken, + }, + cloudflare_auth: { + token: values?.cloudflareToken, + }, aws_auth: { ...values?.aws_auth, }, @@ -242,21 +247,24 @@ export const getCloudRegions = createAsyncThunk< export const getCloudDomains = createAsyncThunk< Array, - string, + { region: string; cloudflareToken?: string }, { dispatch: AppDispatch; state: RootState; } ->('api/getCloudDomains', async (cloudRegion, { getState }) => { +>('api/getCloudDomains', async ({ cloudflareToken, region }, { getState }) => { const { installation: { values, installType }, } = getState(); const res = await axios.post<{ domains: Array }>('/api/proxy', { - url: `/domain/${installType}`, + url: `/domain/${cloudflareToken ? 'cloudflare' : installType}`, body: { ...values, - cloud_region: cloudRegion, + cloud_region: region, + cloudflare_auth: { + token: cloudflareToken, + }, }, }); diff --git a/redux/thunks/git.thunk.ts b/redux/thunks/git.thunk.ts index fbc1f324..b0281b85 100644 --- a/redux/thunks/git.thunk.ts +++ b/redux/thunks/git.thunk.ts @@ -25,7 +25,7 @@ export const getGithubUserOrganizations = createAsyncThunk { return ( - await githubApi.get('/user/orgs', { + await githubApi.get('/user/orgs?per_page=100', { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github+json', @@ -41,7 +41,7 @@ export const getGitHubOrgRepositories = createAsyncThunk< { token: string; organization: string } >('git/getGitHubRepositories', async ({ token, organization }) => { return ( - await githubApi.get(`/orgs/${organization}/repos`, { + await githubApi.get(`/orgs/${organization}/repos?per_page=100`, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github+json', @@ -81,7 +81,7 @@ export const getGitlabGroups = createAsyncThunk( 'git/getGitlabGroups', async (token) => { return ( - await gitlabApi.get('/groups', { + await gitlabApi.get('/groups?per_page=100&top_level_only=true', { headers: { Authorization: `Bearer ${token}`, }, @@ -90,12 +90,25 @@ export const getGitlabGroups = createAsyncThunk( }, ); +export const getGitLabSubgroups = createAsyncThunk< + GitLabProject[], + { token: string; group: string } +>('git/getGitLabSubgroups', async ({ token, group }) => { + return ( + await gitlabApi.get(`/groups/${group}/subgroups?per_page=100`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + ).data; +}); + export const getGitLabProjects = createAsyncThunk< GitLabProject[], { token: string; group: string } >('git/getGitLabProjects', async ({ token, group }) => { return ( - await gitlabApi.get(`/groups/${group}/projects`, { + await gitlabApi.get(`/groups/${group}/projects?per_page=100`, { headers: { Authorization: `Bearer ${token}`, }, diff --git a/types/provision/index.ts b/types/provision/index.ts index 55b1db96..d0573849 100644 --- a/types/provision/index.ts +++ b/types/provision/index.ts @@ -46,8 +46,11 @@ export interface ClusterResponse { cluster_type: ClusterType.MANAGEMENT | ClusterType.WORKLOAD; alerts_email: string; git_provider: string; - git_owner: string; git_user: string; + gitAuth: { + gitOwner: string; + gitToken?: string; + }; last_condition: string; install_tools_check: boolean; domain_liveness_check: boolean; @@ -75,7 +78,6 @@ export interface Cluster extends Row { cloudProvider: InstallationType; cloudRegion: string; domainName: string; - gitOwner: string; gitProvider: string; gitUser: string; gitToken?: string; @@ -83,6 +85,10 @@ export interface Cluster extends Row { creationDate?: string; status?: ClusterStatus; lastErrorCondition: string; + gitAuth: { + gitOwner: string; + gitToken?: string; + }; checks: { install_tools_check: boolean; domain_liveness_check: boolean; diff --git a/types/redux/index.ts b/types/redux/index.ts index d7ba7b52..e31700f9 100644 --- a/types/redux/index.ts +++ b/types/redux/index.ts @@ -10,6 +10,7 @@ export interface AdvancedOptions extends GitValues { useHttps?: boolean; dnsProvider?: string; cloudflareToken?: string; + imageRepository?: string; } export interface AuthValues {