Ver código fonte

Merge pull request #672 from smiclea/coriolis-setup

Show Setup page when launching for the first time
Daniel Vincze 4 anos atrás
pai
commit
b7b28d020f
62 arquivos alterados com 2208 adições e 44 exclusões
  1. 1 0
      .gitignore
  2. 2 1
      config.ts
  3. 1 0
      package.json
  4. 13 2
      server/api/ConfigApi.ts
  5. 1 0
      src/@types/Config.ts
  6. 22 0
      src/@types/InitialSetup.ts
  7. 1 0
      src/@types/declarations.d.ts
  8. 17 5
      src/components/App.tsx
  9. 32 0
      src/components/modules/LicenceModule/LicenceModule.tsx
  10. 143 0
      src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx
  11. 6 0
      src/components/modules/SetupModule/SetupPageEmailBody/package.json
  12. 60 0
      src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx
  13. 6 0
      src/components/modules/SetupModule/SetupPageHelp/package.json
  14. 322 0
      src/components/modules/SetupModule/SetupPageLegal/SetupPageLegal.tsx
  15. 6 0
      src/components/modules/SetupModule/SetupPageLegal/package.json
  16. 19 0
      src/components/modules/SetupModule/SetupPageLegal/resources/transferItemIcon.ts
  17. 125 0
      src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.tsx
  18. 6 0
      src/components/modules/SetupModule/SetupPageLicence/package.json
  19. 54 0
      src/components/modules/SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.tsx
  20. 6 0
      src/components/modules/SetupModule/SetupPageModuleWrapper/package.json
  21. 39 0
      src/components/modules/SetupModule/SetupPageWelcome/SetupPageWelcome.tsx
  22. 6 0
      src/components/modules/SetupModule/SetupPageWelcome/package.json
  23. 31 0
      src/components/modules/SetupModule/resources/cbsl-logo.svg
  24. 250 0
      src/components/modules/SetupModule/resources/countriesList.ts
  25. 56 0
      src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.tsx
  26. 6 0
      src/components/modules/SetupModule/ui/SetupPageBackButton/package.json
  27. 44 0
      src/components/modules/SetupModule/ui/SetupPageInputWrapper/SetupPageInputWrapper.tsx
  28. 6 0
      src/components/modules/SetupModule/ui/SetupPageInputWrapper/package.json
  29. 86 0
      src/components/modules/SetupModule/ui/SetupPageLicenceInput/SetupPageLicenceInput.tsx
  30. 6 0
      src/components/modules/SetupModule/ui/SetupPageLicenceInput/package.json
  31. 8 0
      src/components/modules/SetupModule/ui/SetupPageLicenceInput/resources/licenceImage.svg
  32. 9 0
      src/components/modules/SetupModule/ui/SetupPageLicenceInput/resources/trialLicenceImage.svg
  33. 77 0
      src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.tsx
  34. 6 0
      src/components/modules/SetupModule/ui/SetupPagePasswordStrength/package.json
  35. 58 0
      src/components/modules/SetupModule/ui/SetupPageServerError/SetupPageServerError.tsx
  36. 6 0
      src/components/modules/SetupModule/ui/SetupPageServerError/package.json
  37. 14 0
      src/components/modules/SetupModule/ui/SetupPageServerError/resources/error.svg
  38. 39 0
      src/components/modules/SetupModule/ui/SetupPageTitle/SetupPageTitle.tsx
  39. 6 0
      src/components/modules/SetupModule/ui/SetupPageTitle/package.json
  40. 2 2
      src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx
  41. 1 3
      src/components/modules/WizardModule/WizardPageContent/images/transferItemIcon.ts
  42. 373 0
      src/components/smart/SetupPage/SetupPage.tsx
  43. 6 0
      src/components/smart/SetupPage/package.json
  44. 1 0
      src/components/ui/Arrow/Arrow.tsx
  45. 1 1
      src/components/ui/CopyButton/CopyButton.tsx
  46. 4 1
      src/components/ui/Dropdowns/Dropdown/Dropdown.tsx
  47. 17 13
      src/components/ui/Dropdowns/UserDropdown/UserDropdown.tsx
  48. 1 0
      src/components/ui/LoadingButton/LoadingButton.tsx
  49. 2 2
      src/components/ui/OpenInNewIcon/OpenInNewIcon.ts
  50. 6 0
      src/components/ui/OpenInNewIcon/package.json
  51. 7 6
      src/components/ui/RadioInput/RadioInput.tsx
  52. 1 0
      src/components/ui/TextInput/TextInput.tsx
  53. 5 0
      src/constants.ts
  54. 14 4
      src/sources/LincenceSource.ts
  55. 2 2
      src/stores/LicenceStore.ts
  56. 112 0
      src/stores/SetupStore.ts
  57. 5 1
      src/stores/UserStore.ts
  58. 2 0
      src/utils/ApiCaller.ts
  59. 21 1
      src/utils/Config.ts
  60. 23 0
      src/utils/ObjectUtils.ts
  61. 1 0
      tsconfig.json
  62. 5 0
      yarn.lock

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@ dist
 node_modules
 private/cypress/config.js
 .env
+.not-first-launch

+ 2 - 1
config.ts

@@ -1,4 +1,4 @@
-import type { Config } from './src/@types/Config'
+import type { Config } from '@src/@types/Config'
 
 const conf: Config = {
 
@@ -141,6 +141,7 @@ const conf: Config = {
     coriolisLogs: '{BASE_URL}/logs',
     coriolisLogStreamBaseUrl: '{BASE_URL}',
     coriolisLicensing: '{BASE_URL}/licensing',
+    cloudbaseEmailEndpoint: 'http://localhost:3334',
   },
 }
 

+ 1 - 0
package.json

@@ -92,6 +92,7 @@
     "require-without-cache": "^0.0.6",
     "rimraf": "^2.6.2",
     "styled-components": "^4.4.1",
+    "tai-password-strength": "^1.1.2",
     "typescript": "^4.4.4",
     "url-loader": "^4.1.0",
     "webpack": "^4.41.2",

+ 13 - 2
server/api/ConfigApi.ts

@@ -34,14 +34,17 @@ const modServicesUrls = (configServices: Services, servicesMod?: Services): Serv
   return services
 }
 
+const NOT_FIRST_LAUNCH_PATH = path.join(__dirname, '../../.not-first-launch')
+
 export default (router: express.Router) => {
   router.get('/config', (_, res) => {
     const configPath = path.join(__dirname, '../../config.ts')
+    const isFirstLaunch = !fs.existsSync(NOT_FIRST_LAUNCH_PATH)
     const config: any = requireWithoutCache(configPath, require).config
     const modJsonPath: string | null | undefined = process.env.MOD_JSON
     if (!modJsonPath) {
       config.servicesUrls = modServicesUrls(config.servicesUrls)
-      res.send(config)
+      res.send({ config, isFirstLaunch })
       return
     }
     try {
@@ -53,10 +56,18 @@ export default (router: express.Router) => {
         }
       })
       config.servicesUrls = modServicesUrls(config.servicesUrls, configMod.servicesUrls)
-      res.send(config)
+      res.send({ config, isFirstLaunch })
     } catch (err) {
       console.error(err)
       res.status(400).json({ error: { message: 'Invalid MOD_JSON file' } })
     }
+  }).post('/config/first-launch', (req, res) => {
+    const { isFirstLaunch } = req.body
+    if (isFirstLaunch !== false) {
+      res.status(422).json({ error: { message: '\'isFirstLaunch\' property not set to \'false\'' } })
+      return
+    }
+    fs.writeFileSync(NOT_FIRST_LAUNCH_PATH, '')
+    res.json({ isFirstLaunch })
   })
 }

+ 1 - 0
src/@types/Config.ts

@@ -20,6 +20,7 @@ export type Services = {
   coriolisLogs: string,
   coriolisLogStreamBaseUrl: string,
   coriolisLicensing: string,
+  cloudbaseEmailEndpoint: string
 }
 
 export type Config = {

+ 22 - 0
src/@types/InitialSetup.ts

@@ -0,0 +1,22 @@
+import { ProviderTypes } from './Providers'
+
+export type SetupPageLicenceType = 'paid' | 'trial'
+
+export type CustomerInfoBasic = {
+  fullName: string,
+  email: string,
+  company: string,
+  country: string
+}
+
+export type CustomerInfoTrial = {
+  interestedIn: 'replicas' | 'migrations' | 'both'
+  sourcePlatform: ProviderTypes | null
+  destinationPlatform: ProviderTypes | null
+}
+
+export type CustomerInfoFull = CustomerInfoBasic & CustomerInfoTrial
+
+export const isCustomerInfoFull = (
+  customerInfo: CustomerInfoFull | CustomerInfoBasic,
+): customerInfo is CustomerInfoFull => (<CustomerInfoFull>customerInfo).interestedIn !== undefined

+ 1 - 0
src/@types/declarations.d.ts

@@ -9,6 +9,7 @@ declare module 'ansi-to-html'
 
 declare module 'require-without-cache'
 declare module 'react-transition-group'
+declare module 'tai-password-strength'
 
 interface Window {
   /**

+ 17 - 5
src/components/App.tsx

@@ -14,7 +14,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import { hot } from 'react-hot-loader/root'
 import React from 'react'
-import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
+import {
+  BrowserRouter as Router, Switch, Route,
+} from 'react-router-dom'
 import styled, { createGlobalStyle } from 'styled-components'
 import { observe } from 'mobx'
 
@@ -47,6 +49,7 @@ import { ThemePalette, ThemeProps } from '@src/components/Theme'
 import configLoader from '@src/utils/Config'
 import { navigationMenu } from '@src/constants'
 import userStore from '@src/stores/UserStore'
+import SetupPage from '@src/components/smart/SetupPage'
 
 const GlobalStyle = createGlobalStyle`
  ${Fonts}
@@ -91,7 +94,14 @@ class App extends React.Component<{}, State> {
       this.setState({})
     })
     await configLoader.load()
-    userStore.tokenLogin()
+    if (configLoader.isFirstLaunch && window.location.pathname !== '/login') {
+      if (window.location.pathname !== '/') {
+        window.location.href = '/'
+        return
+      }
+    } else {
+      userStore.tokenLogin()
+    }
     this.setState({ isConfigReady: true })
   }
 
@@ -153,7 +163,7 @@ class App extends React.Component<{}, State> {
           showAuthAnimation: true,
         })
       }
-      if (userStore.loggedUser && userStore.loggedUser.isAdmin === false) {
+      if (userStore.loggedUser?.isAdmin === false) {
         return renderMessagePage({
           path: actualPath,
           exact,
@@ -162,7 +172,7 @@ class App extends React.Component<{}, State> {
           showDenied: true,
         })
       }
-      if (userStore.loggedUser && userStore.loggedUser.isAdmin) {
+      if (userStore.loggedUser?.isAdmin) {
         return <Route path={actualPath} exact={exact} component={component} />
       }
       return null
@@ -173,7 +183,9 @@ class App extends React.Component<{}, State> {
         <GlobalStyle />
         <Router>
           <Switch>
-            {renderRoute('/', DashboardPage, true)}
+            {configLoader.isFirstLaunch ? (
+              <Route path="/" component={SetupPage} exact />
+            ) : renderRoute('/', DashboardPage, true)}
             <Route path="/login" component={LoginPage} />
             {renderRoute('/dashboard', DashboardPage)}
             {renderRoute('/replicas', ReplicasPage, true)}

+ 32 - 0
src/components/modules/LicenceModule/LicenceModule.tsx

@@ -28,6 +28,9 @@ import FileUtils from '@src/utils/FileUtils'
 
 import type { Licence, LicenceServerStatus } from '@src/@types/Licence'
 
+// import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
+import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
+import { LEGAL_URLS } from '@src/constants'
 import licenceImage from './images/licence'
 
 const Wrapper = styled.div<any>`
@@ -99,6 +102,29 @@ const FakeFileInput = styled.input`
   opacity: 0;
   top: -99999px;
 `
+// const OutsideLinkLarge = styled.a`
+//   display: inline-block;
+//   color: ${ThemePalette.primary};
+//   cursor: pointer;
+//   text-decoration: none;
+//   margin-top: 16px;
+// `
+const OutsideLink = styled.a`
+  display: inline-block;
+  color: ${ThemePalette.primary};
+  cursor: pointer;
+  text-decoration: none;
+  margin-top: 8px;
+  font-size: 12px;
+`
+const OpenInNewIconWrapper = styled.div`
+  ${ThemeProps.exactSize('16px')}
+  display: inline-block;
+  position: relative;
+  top: 9px;
+  margin-top: -12px;
+  transform: scale(0.6);
+`
 
 type Props = {
   licenceInfo: Licence | null,
@@ -284,6 +310,12 @@ class LicenceModule extends React.Component<Props, State> {
             </LicenceRowDescription>
           </LicenceRowContent>
         </LicenceRow>
+        <LicenceRow>
+          <OutsideLink href={LEGAL_URLS.eula} target="_blank">
+            Read Coriolis© EULA
+            <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon(ThemePalette.primary) }} />
+          </OutsideLink>
+        </LicenceRow>
       </LicenceInfoWrapper>
     )
   }

+ 143 - 0
src/components/modules/SetupModule/SetupPageEmailBody/SetupPageEmailBody.tsx

@@ -0,0 +1,143 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import { CustomerInfoBasic, CustomerInfoTrial, SetupPageLicenceType } from '@src/@types/InitialSetup'
+import { customerInfoSetupStoreValueToString } from '@src/stores/SetupStore'
+import notificationStore from '@src/stores/NotificationStore'
+import { ThemePalette } from '@src/components/Theme'
+import SetupPageServerError from '@src/components/modules/SetupModule/ui/SetupPageServerError'
+import SetupPageInputWrapper from '@src/components/modules/SetupModule/ui/SetupPageInputWrapper'
+import CopyButton from '@src/components/ui/CopyButton'
+
+const Wrapper = styled.div``
+const Link = styled.a`
+  color: ${ThemePalette.primary};
+  text-decoration: none;
+`
+const EmailBody = styled.div``
+const EmailSubject = styled.div``
+const EmailContent = styled.div`
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+  padding: 4px;
+`
+const EmailBodyContent = styled.div``
+const CopyLink = styled.div`
+  width: 220px;
+  cursor: pointer;
+  margin-top: 8px;
+  display: flex;
+  margin-bottom: 16px;
+`
+
+type Props = {
+  customerInfoBasic: CustomerInfoBasic
+  customerInfoTrial: CustomerInfoTrial
+  licenceType: SetupPageLicenceType
+  applianceId: string
+}
+
+@observer
+class SetupPageEmailBody extends React.Component<Props> {
+  emailTemplate: HTMLElement | null = null
+
+  handleCopy(event?: React.ClipboardEvent) {
+    event?.preventDefault()
+
+    if (!this.emailTemplate) {
+      return
+    }
+
+    try {
+      const range = document.createRange()
+      range.selectNode(this.emailTemplate)
+      window.getSelection()?.removeAllRanges()
+      window.getSelection()?.addRange(range)
+      document.execCommand('copy')
+      if (!event) {
+        notificationStore.alert('The email body was succesfully copied to clipboard', 'success')
+      }
+    } catch (err) {
+      notificationStore.alert('Error copying to clipboard', 'error')
+    }
+  }
+
+  render() {
+    const emailTemplate = (
+      <div>
+        Hi,<br /><br />
+        I would like to request a Coriolis Licence with the following info:<br /><br />
+        <b>Full Name</b>: {this.props.customerInfoBasic.fullName}<br />
+        <b>Email</b>: {this.props.customerInfoBasic.email}<br />
+        <b>Company</b>: {this.props.customerInfoBasic.company}<br />
+        <b>Country</b>: {this.props.customerInfoBasic.country}<br />
+        {this.props.licenceType === 'trial' ? (
+          <>
+            <b>Interested In</b>: {customerInfoSetupStoreValueToString('interestedIn', this.props.customerInfoTrial.interestedIn)}<br />
+            <b>Source Platform</b>: {customerInfoSetupStoreValueToString('sourcePlatform', this.props.customerInfoTrial.sourcePlatform)}<br />
+            <b>Destination Platform</b>: {customerInfoSetupStoreValueToString('destinationPlatform', this.props.customerInfoTrial.destinationPlatform)}<br />
+          </>
+        ) : null}
+        <b>Licence Type</b>: {this.props.licenceType}<br />
+        <b>Appliance ID</b>: {this.props.applianceId}<br />
+        <br />
+        Regards,<br />
+        {this.props.customerInfoBasic.fullName}
+      </div>
+    )
+    return (
+      <Wrapper>
+        <SetupPageServerError style={{ marginTop: '-16px' }}>
+          <p>There was an error submitting the form to Cloudbase Solutions support.</p>
+          <p>Please send the following email body to <Link href="mailto:support@cloudbase.it" target="_blank">support@cloudbase.it</Link>.</p>
+        </SetupPageServerError>
+        <EmailBody>
+          <EmailSubject>
+            <SetupPageInputWrapper label="Subject">
+              <EmailContent>
+                Coriolis Licence Request
+              </EmailContent>
+            </SetupPageInputWrapper>
+          </EmailSubject>
+          <EmailBodyContent>
+            <SetupPageInputWrapper label="Body">
+              <EmailContent onCopy={e => { this.handleCopy(e) }}>
+                {emailTemplate}
+              </EmailContent>
+            </SetupPageInputWrapper>
+            <CopyLink onClick={() => { this.handleCopy() }}>
+              <CopyButton style={{ opacity: 1, marginRight: '8px' }} />Copy email body to clipboard
+            </CopyLink>
+          </EmailBodyContent>
+        </EmailBody>
+        <div
+          style={{
+            position: 'absolute',
+            top: '-100000px',
+            color: 'black',
+            background: 'white',
+          }}
+          ref={ref => { this.emailTemplate = ref }}
+        >
+          {emailTemplate}
+        </div>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageEmailBody

+ 6 - 0
src/components/modules/SetupModule/SetupPageEmailBody/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageEmailBody",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageEmailBody.tsx"
+}

+ 60 - 0
src/components/modules/SetupModule/SetupPageHelp/SetupPageHelp.tsx

@@ -0,0 +1,60 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import SetupPageTitle from '@src/components/modules/SetupModule/ui/SetupPageTitle'
+import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
+
+const Wrapper = styled.div``
+const Help = styled.a`
+  display: flex;
+  align-items: center;
+  color: ${ThemePalette.primary};
+  cursor: pointer;
+  text-decoration: none;
+  max-width: 190px;
+  margin-bottom: 16px;
+`
+const OpenInNewIconWrapper = styled.div`
+  ${ThemeProps.exactSize('16px')}
+  position: relative;
+  top: -2px;
+  transform: scale(0.6);
+`
+type Props = {
+  style: React.CSSProperties
+}
+
+@observer
+class SetupPageHelp extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper style={this.props.style}>
+        <SetupPageTitle title="Coriolis® Help" />
+        <p>
+          Click the link below to view the Coriolis® documentation. There you can find all the help you need to get you started.
+        </p>
+        <Help href="https://cloudbase.it/coriolis-overview/" target="_balnk">
+          Coriolis® Documentation
+          <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon(ThemePalette.primary) }} />
+        </Help>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageHelp

+ 6 - 0
src/components/modules/SetupModule/SetupPageHelp/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageHelp",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageHelp.tsx"
+}

+ 322 - 0
src/components/modules/SetupModule/SetupPageLegal/SetupPageLegal.tsx

@@ -0,0 +1,322 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import { ProviderTypes } from '@src/@types/Providers'
+import configLoader from '@src/utils/Config'
+import { CustomerInfoTrial, SetupPageLicenceType } from '@src/@types/InitialSetup'
+import SetupPageInputWrapper from '@src/components/modules/SetupModule/ui/SetupPageInputWrapper'
+import RadioInput from '@src/components/ui/RadioInput'
+import Dropdown from '@src/components/ui/Dropdowns/Dropdown'
+import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
+import SetupPageTitle from '@src/components/modules/SetupModule/ui/SetupPageTitle'
+import Checkbox from '@src/components/ui/Checkbox'
+import { LEGAL_URLS } from '@src/constants'
+import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
+import transferItemIcon from './resources/transferItemIcon'
+
+const Wrapper = styled.div``
+const TrialForm = styled.div``
+const AgreementForm = styled.div`
+  margin-top: 24px;
+  > div {
+    margin-top: 8px;
+  }
+`
+const RadioGroup = styled.div`
+  display: flex;
+  justify-content: space-between;
+  margin-top: 4px;
+`
+const RadioInputLabel = styled.div`
+  display: flex;
+`
+const TransferItemIcon = styled.div`
+  ${ThemeProps.exactSize('16px')}
+  display: flex;
+  align-items: center;
+  margin-right: 4px;
+`
+const PlatformItemRenderer = styled.div`
+  display: flex;
+  align-items: center;
+`
+const PlatformLogoBackground = styled.div<{ whiteBackground: boolean }>`
+  ${props => (props.whiteBackground ? css`background: white;` : '')}
+  margin-left: -15px;
+  margin-top: -7px;
+  padding-top: 7px;
+  margin-bottom: -7px;
+  padding-bottom: 7px;
+  padding-left: 4px;
+  margin-right: 8px;
+  > div:first-child {
+    transform: scale(0.6);
+  }
+`
+const CheckboxWrapper = styled.div`
+  display: flex;
+`
+const CheckboxLabel = styled.div`
+  margin-left: 8px;
+  cursor: pointer;
+  display: inline-block;
+`
+const CheckboxLink = styled.a`
+  display: inline-block;
+  color: ${ThemePalette.primary};
+  cursor: pointer;
+  text-decoration: none;
+`
+const OpenInNewIconWrapper = styled.div`
+  ${ThemeProps.exactSize('16px')}
+  display: inline-block;
+  position: relative;
+  top: 9px;
+  margin-top: -12px;
+  transform: scale(0.6);
+`
+
+const SOURCE_PLATFORMS: ProviderTypes[] = ['aws', 'openstack', 'vmware_vsphere', 'azure', 'hyper-v', 'oci', 'oracle_vm']
+const DESTINATION_PLATFORMS: ProviderTypes[] = ['aws', 'openstack', 'vmware_vsphere', 'azure', 'scvmm', 'oci', 'opc', 'oracle_vm']
+
+type PlatformDropdownItemType = { value: ProviderTypes | null, label: string }
+
+const preparePlatformItems = (providers: ProviderTypes[]) => {
+  const mappedItems = providers.map(provider => ({
+    value: provider,
+    label: configLoader.config.providerNames[provider],
+  }))
+  const sortPriority = configLoader.config.providerSortPriority
+  mappedItems.sort((a, b) => {
+    if (sortPriority[a.value] && sortPriority[b.value]) {
+      return (sortPriority[a.value] - sortPriority[b.value]) || a.value.localeCompare(b.value)
+    }
+    if (sortPriority[a.value]) {
+      return -1
+    }
+    if (sortPriority[b.value]) {
+      return 1
+    }
+    return a.value.localeCompare(b.value)
+  })
+  const mappedItemsNullabled = mappedItems as PlatformDropdownItemType[]
+  mappedItemsNullabled.unshift({ value: null, label: 'Choose Platform' })
+  return mappedItemsNullabled
+}
+const getPlatformLabelForValue = (value: ProviderTypes | null) => (value ? configLoader.config.providerNames[value] : 'Choose Platform')
+
+type State = {
+  privacyAgreement: boolean
+  eulaAgreement: boolean
+}
+
+type Props = {
+  licenceType: SetupPageLicenceType
+  customerInfoTrial: CustomerInfoTrial
+  onCustomerInfoChange: (field: keyof CustomerInfoTrial, value: any) => void
+  onLegalChange: (accepted: boolean) => void
+}
+
+@observer
+class SetupPageLegal extends React.Component<Props, State> {
+  state = {
+    privacyAgreement: false,
+    eulaAgreement: false,
+  } as State
+
+  _sourcePlatformItems: PlatformDropdownItemType[] = []
+
+  _destinationPlatformItems: PlatformDropdownItemType[] = []
+
+  get sourcePlatformItems() {
+    if (this._sourcePlatformItems.length) {
+      return this._sourcePlatformItems
+    }
+    this._sourcePlatformItems = preparePlatformItems(SOURCE_PLATFORMS)
+    return this._sourcePlatformItems
+  }
+
+  get destinationPlatformItems() {
+    if (this._destinationPlatformItems.length) {
+      return this._destinationPlatformItems
+    }
+    this._destinationPlatformItems = preparePlatformItems(DESTINATION_PLATFORMS)
+    return this._destinationPlatformItems
+  }
+
+  handleLegalChange() {
+    this.props.onLegalChange(this.state.privacyAgreement && this.state.eulaAgreement)
+  }
+
+  handlePrivacyChange(privacyAgreement: boolean) {
+    this.setState({ privacyAgreement }, () => {
+      this.handleLegalChange()
+    })
+  }
+
+  handleEulaChange(eulaAgreement: boolean) {
+    this.setState({ eulaAgreement }, () => {
+      this.handleLegalChange()
+    })
+  }
+
+  renderTrialForm() {
+    return (
+      <TrialForm>
+        <SetupPageInputWrapper label="Interested in">
+          <RadioGroup>
+            <RadioInput
+              label={(
+                <RadioInputLabel>
+                  <TransferItemIcon
+                    dangerouslySetInnerHTML={{ __html: transferItemIcon(ThemePalette.primary) }}
+                  />
+                  Migrations
+                </RadioInputLabel>
+              )}
+              checked={this.props.customerInfoTrial.interestedIn === 'migrations'}
+              onChange={checked => {
+                if (checked) {
+                  this.props.onCustomerInfoChange('interestedIn', 'migrations')
+                }
+              }}
+            />
+            <RadioInput
+              label={(
+                <RadioInputLabel>
+                  <TransferItemIcon
+                    dangerouslySetInnerHTML={{ __html: transferItemIcon(ThemePalette.alert) }}
+                  />
+                  Replicas
+                </RadioInputLabel>
+              )}
+              checked={this.props.customerInfoTrial.interestedIn === 'replicas'}
+              onChange={checked => {
+                if (checked) {
+                  this.props.onCustomerInfoChange('interestedIn', 'replicas')
+                }
+              }}
+            />
+            <RadioInput
+              label="Both"
+              checked={this.props.customerInfoTrial.interestedIn === 'both'}
+              onChange={checked => {
+                if (checked) {
+                  this.props.onCustomerInfoChange('interestedIn', 'both')
+                }
+              }}
+            />
+          </RadioGroup>
+        </SetupPageInputWrapper>
+        <SetupPageInputWrapper label="Source Platform">
+          <Dropdown
+            width={450}
+            items={this.sourcePlatformItems}
+            // eslint-disable-next-line react/no-unstable-nested-components
+            labelRenderer={(item: PlatformDropdownItemType, idx: number) => (
+              <PlatformItemRenderer>
+                <PlatformLogoBackground whiteBackground={idx > 0}>
+                  <EndpointLogos
+                    endpoint={item.value}
+                    height={32}
+                  />
+                </PlatformLogoBackground>
+                {item.label}
+              </PlatformItemRenderer>
+            )}
+            selectedItem={{
+              value: this.props.customerInfoTrial.sourcePlatform,
+              label: getPlatformLabelForValue(this.props.customerInfoTrial.sourcePlatform),
+            }}
+            onChange={item => { this.props.onCustomerInfoChange('sourcePlatform', item.value) }}
+          />
+        </SetupPageInputWrapper>
+        <SetupPageInputWrapper label="Destination Platform">
+          <Dropdown
+            width={450}
+            items={this.destinationPlatformItems}
+            // eslint-disable-next-line react/no-unstable-nested-components
+            labelRenderer={(item: PlatformDropdownItemType, idx: number) => (
+              <PlatformItemRenderer>
+                <PlatformLogoBackground whiteBackground={idx > 0}>
+                  <EndpointLogos
+                    endpoint={item.value}
+                    height={32}
+                  />
+                </PlatformLogoBackground>
+                {item.label}
+              </PlatformItemRenderer>
+            )}
+            selectedItem={{
+              value: this.props.customerInfoTrial.destinationPlatform,
+              label: getPlatformLabelForValue(this.props.customerInfoTrial.destinationPlatform),
+            }}
+            onChange={item => { this.props.onCustomerInfoChange('destinationPlatform', item.value) }}
+          />
+        </SetupPageInputWrapper>
+      </TrialForm>
+    )
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <SetupPageTitle title={this.props.licenceType === 'trial' ? 'Coriolis® Trial License' : 'Coriolis® Agreement'} />
+        {this.props.licenceType === 'trial' ? this.renderTrialForm() : null}
+        <AgreementForm>
+          <CheckboxWrapper>
+            <Checkbox
+              checked={this.state.privacyAgreement}
+              onChange={privacyAgreement => { this.handlePrivacyChange(privacyAgreement) }}
+            />
+            <CheckboxLabel onClick={() => { this.handlePrivacyChange(!this.state.privacyAgreement) }}>
+              By submitting I agree to the usage of my data according to the&nbsp;
+              <CheckboxLink
+                href={LEGAL_URLS.privacy}
+                target="_blank"
+                onClick={e => { e.stopPropagation() }}
+              >
+                Privacy Policy
+                <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon(ThemePalette.primary) }} />
+              </CheckboxLink>.
+            </CheckboxLabel>
+          </CheckboxWrapper>
+          <CheckboxWrapper>
+            <Checkbox
+              checked={this.state.eulaAgreement}
+              onChange={eulaAgreement => { this.handleEulaChange(eulaAgreement) }}
+            />
+            <CheckboxLabel onClick={() => { this.handleEulaChange(!this.state.eulaAgreement) }}>
+              By submitting I agree to the&nbsp;
+              <CheckboxLink
+                href={LEGAL_URLS.eula}
+                target="_blank"
+                onClick={e => { e.stopPropagation() }}
+              >
+                Coriolis® EULA
+                <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon(ThemePalette.primary) }} />
+              </CheckboxLink>.
+            </CheckboxLabel>
+          </CheckboxWrapper>
+        </AgreementForm>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageLegal

+ 6 - 0
src/components/modules/SetupModule/SetupPageLegal/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageLegal",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageLegal.tsx"
+}

Diferenças do arquivo suprimidas por serem muito extensas
+ 19 - 0
src/components/modules/SetupModule/SetupPageLegal/resources/transferItemIcon.ts


+ 125 - 0
src/components/modules/SetupModule/SetupPageLicence/SetupPageLicence.tsx

@@ -0,0 +1,125 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import countriesList from '@src/components/modules/SetupModule/resources/countriesList'
+import { CustomerInfoBasic, SetupPageLicenceType } from '@src/@types/InitialSetup'
+import SetupPageTitle from '@src/components/modules/SetupModule/ui/SetupPageTitle'
+import SetupPageInputWrapper from '@src/components/modules/SetupModule/ui/SetupPageInputWrapper'
+import TextInput from '@src/components/ui/TextInput'
+import AutocompleteDropdown from '@src/components/ui/Dropdowns/AutocompleteDropdown'
+import SetupPageLicenceInput from '@src/components/modules/SetupModule/ui/SetupPageLicenceInput'
+
+const Wrapper = styled.div``
+const Header = styled.div`
+  margin-bottom: 16px;
+`
+const Form = styled.form``
+// const LicenceTypeRadioGroup = styled.div`
+//   display: flex;
+//   margin-top: 4px;
+//   margin-left: -16px;
+//   > div {
+//     margin-left: 16px;
+//   }
+// `
+
+type CountryDropdownItemType = {
+  label: string
+  value: string
+}
+const prepareCountriesItems = () => countriesList.map(c => ({ label: c.name, value: c.code }))
+
+type Props = {
+  customerInfo: CustomerInfoBasic
+  onUpdateCustomerInfo: (field: keyof CustomerInfoBasic, value: any) => void
+  highlightEmptyFields: boolean
+  highlightEmail: boolean
+  onSubmit: () => void
+  licenceType: SetupPageLicenceType
+  onLicenceTypeChange: (type: SetupPageLicenceType) => void
+}
+@observer
+class SetupPageLicence extends React.Component<Props> {
+  _countriesItems: CountryDropdownItemType[] = []
+
+  get countriesItems() {
+    if (this._countriesItems.length) {
+      return this._countriesItems
+    }
+    this._countriesItems = prepareCountriesItems()
+    return this._countriesItems
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <SetupPageTitle title="Coriolis® Licence" />
+        <Header>
+          In order to obtain a licence, please fill in the following form.
+        </Header>
+        <Form onSubmit={e => { e.preventDefault(); this.props.onSubmit() }}>
+          <SetupPageInputWrapper label="Full name">
+            <TextInput
+              width="100%"
+              value={this.props.customerInfo.fullName}
+              onChange={e => { this.props.onUpdateCustomerInfo('fullName', e.target.value) }}
+              required
+              highlight={this.props.highlightEmptyFields && !this.props.customerInfo.fullName}
+            />
+          </SetupPageInputWrapper>
+          <SetupPageInputWrapper label="Email">
+            <TextInput
+              width="100%"
+              required
+              type="email"
+              value={this.props.customerInfo.email}
+              onChange={e => { this.props.onUpdateCustomerInfo('email', e.target.value) }}
+              highlight={(this.props.highlightEmptyFields && !this.props.customerInfo.email) || this.props.highlightEmail}
+            />
+          </SetupPageInputWrapper>
+          <SetupPageInputWrapper label="Company">
+            <TextInput
+              width="100%"
+              required
+              value={this.props.customerInfo.company}
+              onChange={e => { this.props.onUpdateCustomerInfo('company', e.target.value) }}
+              highlight={this.props.highlightEmptyFields && !this.props.customerInfo.company}
+            />
+          </SetupPageInputWrapper>
+          <SetupPageInputWrapper label="Country">
+            <AutocompleteDropdown
+              required
+              width={450}
+              items={this.countriesItems}
+              selectedItem={this.countriesItems.find(c => c.label === this.props.customerInfo.country)}
+              onChange={item => { this.props.onUpdateCustomerInfo('country', item.label) }}
+              highlight={this.props.highlightEmptyFields && !this.props.customerInfo.country}
+            />
+          </SetupPageInputWrapper>
+          <SetupPageLicenceInput
+            style={{ marginTop: '24px', justifyContent: 'center' }}
+            licenceType={this.props.licenceType}
+            onLicenceTypeChange={this.props.onLicenceTypeChange}
+          />
+          <button style={{ display: 'none' }} type="submit">Submit</button>
+        </Form>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageLicence

+ 6 - 0
src/components/modules/SetupModule/SetupPageLicence/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageLicence",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageLicence.tsx"
+}

+ 54 - 0
src/components/modules/SetupModule/SetupPageModuleWrapper/SetupPageModuleWrapper.tsx

@@ -0,0 +1,54 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import { ThemeProps } from '@src/components/Theme'
+
+const Wrapper = styled.div`
+  background: rgba(221, 224, 229, 0.5);
+  padding: 16px 32px 32px 32px;
+  border-radius: 8px;
+  color: white;
+  margin-top: 32px;
+  ${ThemeProps.exactWidth('450px')}
+`
+const Content = styled.div``
+const Actions = styled.div`
+  margin-top: 24px;
+  display: flex;
+`
+
+type Props = {
+  children: React.ReactNode
+  actions: React.ReactNode
+  actionsWrapperStyle?: React.CSSProperties
+}
+
+@observer
+class SetupPageModuleWrapper extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper>
+        <Content>{this.props.children}</Content>
+        <Actions>
+          {this.props.actions}
+        </Actions>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageModuleWrapper

+ 6 - 0
src/components/modules/SetupModule/SetupPageModuleWrapper/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageModuleWrapper",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageModuleWrapper.tsx"
+}

+ 39 - 0
src/components/modules/SetupModule/SetupPageWelcome/SetupPageWelcome.tsx

@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import SetupPageTitle from '@src/components/modules/SetupModule/ui/SetupPageTitle'
+
+const Wrapper = styled.div``
+
+type Props = {}
+
+@observer
+class SetupPageWelcome extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper>
+        <SetupPageTitle title="Welcome to Coriolis®" />
+        <p>
+          Coriolis® is a fully distributed and scalable system that provides both “lift-and-shift” migration services (CMaaS) and cross-site disaster
+          recovery features (DRaaS) between a source cloud platform and an independent destination cloud platform.
+        </p>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageWelcome

+ 6 - 0
src/components/modules/SetupModule/SetupPageWelcome/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageWelcome",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageWelcome.tsx"
+}

Diferenças do arquivo suprimidas por serem muito extensas
+ 31 - 0
src/components/modules/SetupModule/resources/cbsl-logo.svg


+ 250 - 0
src/components/modules/SetupModule/resources/countriesList.ts

@@ -0,0 +1,250 @@
+export type Country = {
+  name: string,
+  code: string,
+}
+
+export default [
+  { name: 'Afghanistan', code: 'AF' },
+  { name: 'Åland Islands', code: 'AX' },
+  { name: 'Albania', code: 'AL' },
+  { name: 'Algeria', code: 'DZ' },
+  { name: 'American Samoa', code: 'AS' },
+  { name: 'AndorrA', code: 'AD' },
+  { name: 'Angola', code: 'AO' },
+  { name: 'Anguilla', code: 'AI' },
+  { name: 'Antarctica', code: 'AQ' },
+  { name: 'Antigua and Barbuda', code: 'AG' },
+  { name: 'Argentina', code: 'AR' },
+  { name: 'Armenia', code: 'AM' },
+  { name: 'Aruba', code: 'AW' },
+  { name: 'Australia', code: 'AU' },
+  { name: 'Austria', code: 'AT' },
+  { name: 'Azerbaijan', code: 'AZ' },
+  { name: 'Bahamas', code: 'BS' },
+  { name: 'Bahrain', code: 'BH' },
+  { name: 'Bangladesh', code: 'BD' },
+  { name: 'Barbados', code: 'BB' },
+  { name: 'Belarus', code: 'BY' },
+  { name: 'Belgium', code: 'BE' },
+  { name: 'Belize', code: 'BZ' },
+  { name: 'Benin', code: 'BJ' },
+  { name: 'Bermuda', code: 'BM' },
+  { name: 'Bhutan', code: 'BT' },
+  { name: 'Bolivia', code: 'BO' },
+  { name: 'Bosnia and Herzegovina', code: 'BA' },
+  { name: 'Botswana', code: 'BW' },
+  { name: 'Bouvet Island', code: 'BV' },
+  { name: 'Brazil', code: 'BR' },
+  { name: 'British Indian Ocean Territory', code: 'IO' },
+  { name: 'Brunei Darussalam', code: 'BN' },
+  { name: 'Bulgaria', code: 'BG' },
+  { name: 'Burkina Faso', code: 'BF' },
+  { name: 'Burundi', code: 'BI' },
+  { name: 'Cambodia', code: 'KH' },
+  { name: 'Cameroon', code: 'CM' },
+  { name: 'Canada', code: 'CA' },
+  { name: 'Cape Verde', code: 'CV' },
+  { name: 'Cayman Islands', code: 'KY' },
+  { name: 'Central African Republic', code: 'CF' },
+  { name: 'Chad', code: 'TD' },
+  { name: 'Chile', code: 'CL' },
+  { name: 'China', code: 'CN' },
+  { name: 'Christmas Island', code: 'CX' },
+  { name: 'Cocos (Keeling) Islands', code: 'CC' },
+  { name: 'Colombia', code: 'CO' },
+  { name: 'Comoros', code: 'KM' },
+  { name: 'Congo', code: 'CG' },
+  { name: 'Congo, The Democratic Republic of the', code: 'CD' },
+  { name: 'Cook Islands', code: 'CK' },
+  { name: 'Costa Rica', code: 'CR' },
+  { name: 'Cote D\'Ivoire', code: 'CI' },
+  { name: 'Croatia', code: 'HR' },
+  { name: 'Cuba', code: 'CU' },
+  { name: 'Cyprus', code: 'CY' },
+  { name: 'Czech Republic', code: 'CZ' },
+  { name: 'Denmark', code: 'DK' },
+  { name: 'Djibouti', code: 'DJ' },
+  { name: 'Dominica', code: 'DM' },
+  { name: 'Dominican Republic', code: 'DO' },
+  { name: 'Ecuador', code: 'EC' },
+  { name: 'Egypt', code: 'EG' },
+  { name: 'El Salvador', code: 'SV' },
+  { name: 'Equatorial Guinea', code: 'GQ' },
+  { name: 'Eritrea', code: 'ER' },
+  { name: 'Estonia', code: 'EE' },
+  { name: 'Ethiopia', code: 'ET' },
+  { name: 'Falkland Islands (Malvinas)', code: 'FK' },
+  { name: 'Faroe Islands', code: 'FO' },
+  { name: 'Fiji', code: 'FJ' },
+  { name: 'Finland', code: 'FI' },
+  { name: 'France', code: 'FR' },
+  { name: 'French Guiana', code: 'GF' },
+  { name: 'French Polynesia', code: 'PF' },
+  { name: 'French Southern Territories', code: 'TF' },
+  { name: 'Gabon', code: 'GA' },
+  { name: 'Gambia', code: 'GM' },
+  { name: 'Georgia', code: 'GE' },
+  { name: 'Germany', code: 'DE' },
+  { name: 'Ghana', code: 'GH' },
+  { name: 'Gibraltar', code: 'GI' },
+  { name: 'Greece', code: 'GR' },
+  { name: 'Greenland', code: 'GL' },
+  { name: 'Grenada', code: 'GD' },
+  { name: 'Guadeloupe', code: 'GP' },
+  { name: 'Guam', code: 'GU' },
+  { name: 'Guatemala', code: 'GT' },
+  { name: 'Guernsey', code: 'GG' },
+  { name: 'Guinea', code: 'GN' },
+  { name: 'Guinea-Bissau', code: 'GW' },
+  { name: 'Guyana', code: 'GY' },
+  { name: 'Haiti', code: 'HT' },
+  { name: 'Heard Island and Mcdonald Islands', code: 'HM' },
+  { name: 'Holy See (Vatican City State)', code: 'VA' },
+  { name: 'Honduras', code: 'HN' },
+  { name: 'Hong Kong', code: 'HK' },
+  { name: 'Hungary', code: 'HU' },
+  { name: 'Iceland', code: 'IS' },
+  { name: 'India', code: 'IN' },
+  { name: 'Indonesia', code: 'ID' },
+  { name: 'Iran, Islamic Republic Of', code: 'IR' },
+  { name: 'Iraq', code: 'IQ' },
+  { name: 'Ireland', code: 'IE' },
+  { name: 'Isle of Man', code: 'IM' },
+  { name: 'Israel', code: 'IL' },
+  { name: 'Italy', code: 'IT' },
+  { name: 'Jamaica', code: 'JM' },
+  { name: 'Japan', code: 'JP' },
+  { name: 'Jersey', code: 'JE' },
+  { name: 'Jordan', code: 'JO' },
+  { name: 'Kazakhstan', code: 'KZ' },
+  { name: 'Kenya', code: 'KE' },
+  { name: 'Kiribati', code: 'KI' },
+  { name: 'Korea, Democratic People\'S Republic of', code: 'KP' },
+  { name: 'Korea, Republic of', code: 'KR' },
+  { name: 'Kuwait', code: 'KW' },
+  { name: 'Kyrgyzstan', code: 'KG' },
+  { name: 'Lao People\'S Democratic Republic', code: 'LA' },
+  { name: 'Latvia', code: 'LV' },
+  { name: 'Lebanon', code: 'LB' },
+  { name: 'Lesotho', code: 'LS' },
+  { name: 'Liberia', code: 'LR' },
+  { name: 'Libyan Arab Jamahiriya', code: 'LY' },
+  { name: 'Liechtenstein', code: 'LI' },
+  { name: 'Lithuania', code: 'LT' },
+  { name: 'Luxembourg', code: 'LU' },
+  { name: 'Macao', code: 'MO' },
+  { name: 'Macedonia, The Former Yugoslav Republic of', code: 'MK' },
+  { name: 'Madagascar', code: 'MG' },
+  { name: 'Malawi', code: 'MW' },
+  { name: 'Malaysia', code: 'MY' },
+  { name: 'Maldives', code: 'MV' },
+  { name: 'Mali', code: 'ML' },
+  { name: 'Malta', code: 'MT' },
+  { name: 'Marshall Islands', code: 'MH' },
+  { name: 'Martinique', code: 'MQ' },
+  { name: 'Mauritania', code: 'MR' },
+  { name: 'Mauritius', code: 'MU' },
+  { name: 'Mayotte', code: 'YT' },
+  { name: 'Mexico', code: 'MX' },
+  { name: 'Micronesia, Federated States of', code: 'FM' },
+  { name: 'Moldova, Republic of', code: 'MD' },
+  { name: 'Monaco', code: 'MC' },
+  { name: 'Mongolia', code: 'MN' },
+  { name: 'Montserrat', code: 'MS' },
+  { name: 'Morocco', code: 'MA' },
+  { name: 'Mozambique', code: 'MZ' },
+  { name: 'Myanmar', code: 'MM' },
+  { name: 'Namibia', code: 'NA' },
+  { name: 'Nauru', code: 'NR' },
+  { name: 'Nepal', code: 'NP' },
+  { name: 'Netherlands', code: 'NL' },
+  { name: 'Netherlands Antilles', code: 'AN' },
+  { name: 'New Caledonia', code: 'NC' },
+  { name: 'New Zealand', code: 'NZ' },
+  { name: 'Nicaragua', code: 'NI' },
+  { name: 'Niger', code: 'NE' },
+  { name: 'Nigeria', code: 'NG' },
+  { name: 'Niue', code: 'NU' },
+  { name: 'Norfolk Island', code: 'NF' },
+  { name: 'Northern Mariana Islands', code: 'MP' },
+  { name: 'Norway', code: 'NO' },
+  { name: 'Oman', code: 'OM' },
+  { name: 'Pakistan', code: 'PK' },
+  { name: 'Palau', code: 'PW' },
+  { name: 'Palestinian Territory, Occupied', code: 'PS' },
+  { name: 'Panama', code: 'PA' },
+  { name: 'Papua New Guinea', code: 'PG' },
+  { name: 'Paraguay', code: 'PY' },
+  { name: 'Peru', code: 'PE' },
+  { name: 'Philippines', code: 'PH' },
+  { name: 'Pitcairn', code: 'PN' },
+  { name: 'Poland', code: 'PL' },
+  { name: 'Portugal', code: 'PT' },
+  { name: 'Puerto Rico', code: 'PR' },
+  { name: 'Qatar', code: 'QA' },
+  { name: 'Reunion', code: 'RE' },
+  { name: 'Romania', code: 'RO' },
+  { name: 'Russian Federation', code: 'RU' },
+  { name: 'RWANDA', code: 'RW' },
+  { name: 'Saint Helena', code: 'SH' },
+  { name: 'Saint Kitts and Nevis', code: 'KN' },
+  { name: 'Saint Lucia', code: 'LC' },
+  { name: 'Saint Pierre and Miquelon', code: 'PM' },
+  { name: 'Saint Vincent and the Grenadines', code: 'VC' },
+  { name: 'Samoa', code: 'WS' },
+  { name: 'San Marino', code: 'SM' },
+  { name: 'Sao Tome and Principe', code: 'ST' },
+  { name: 'Saudi Arabia', code: 'SA' },
+  { name: 'Senegal', code: 'SN' },
+  { name: 'Serbia and Montenegro', code: 'CS' },
+  { name: 'Seychelles', code: 'SC' },
+  { name: 'Sierra Leone', code: 'SL' },
+  { name: 'Singapore', code: 'SG' },
+  { name: 'Slovakia', code: 'SK' },
+  { name: 'Slovenia', code: 'SI' },
+  { name: 'Solomon Islands', code: 'SB' },
+  { name: 'Somalia', code: 'SO' },
+  { name: 'South Africa', code: 'ZA' },
+  { name: 'South Georgia and the South Sandwich Islands', code: 'GS' },
+  { name: 'Spain', code: 'ES' },
+  { name: 'Sri Lanka', code: 'LK' },
+  { name: 'Sudan', code: 'SD' },
+  { name: 'Suriname', code: 'SR' },
+  { name: 'Svalbard and Jan Mayen', code: 'SJ' },
+  { name: 'Swaziland', code: 'SZ' },
+  { name: 'Sweden', code: 'SE' },
+  { name: 'Switzerland', code: 'CH' },
+  { name: 'Syrian Arab Republic', code: 'SY' },
+  { name: 'Taiwan, Province of China', code: 'TW' },
+  { name: 'Tajikistan', code: 'TJ' },
+  { name: 'Tanzania, United Republic of', code: 'TZ' },
+  { name: 'Thailand', code: 'TH' },
+  { name: 'Timor-Leste', code: 'TL' },
+  { name: 'Togo', code: 'TG' },
+  { name: 'Tokelau', code: 'TK' },
+  { name: 'Tonga', code: 'TO' },
+  { name: 'Trinidad and Tobago', code: 'TT' },
+  { name: 'Tunisia', code: 'TN' },
+  { name: 'Turkey', code: 'TR' },
+  { name: 'Turkmenistan', code: 'TM' },
+  { name: 'Turks and Caicos Islands', code: 'TC' },
+  { name: 'Tuvalu', code: 'TV' },
+  { name: 'Uganda', code: 'UG' },
+  { name: 'Ukraine', code: 'UA' },
+  { name: 'United Arab Emirates', code: 'AE' },
+  { name: 'United Kingdom', code: 'GB' },
+  { name: 'United States', code: 'US' },
+  { name: 'United States Minor Outlying Islands', code: 'UM' },
+  { name: 'Uruguay', code: 'UY' },
+  { name: 'Uzbekistan', code: 'UZ' },
+  { name: 'Vanuatu', code: 'VU' },
+  { name: 'Venezuela', code: 'VE' },
+  { name: 'Viet Nam', code: 'VN' },
+  { name: 'Virgin Islands, British', code: 'VG' },
+  { name: 'Virgin Islands, U.S.', code: 'VI' },
+  { name: 'Wallis and Futuna', code: 'WF' },
+  { name: 'Western Sahara', code: 'EH' },
+  { name: 'Yemen', code: 'YE' },
+  { name: 'Zambia', code: 'ZM' },
+  { name: 'Zimbabwe', code: 'ZW' },
+]

+ 56 - 0
src/components/modules/SetupModule/ui/SetupPageBackButton/SetupPageBackButton.tsx

@@ -0,0 +1,56 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import { ThemePalette } from '@src/components/Theme'
+import Arrow from '@src/components/ui/Arrow'
+
+const Wrapper = styled.div`
+  color: ${ThemePalette.primary};
+  width: 98px;
+  height: 100%;
+  justify-content: center;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 6px;
+  padding: 0 8px 0 3px;
+  &:hover {
+    background: rgba(255, 255, 255, 0.2);
+  }
+`
+type Props = {
+  onClick: () => void
+}
+
+@observer
+class SetupPageBackButton extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper onClick={this.props.onClick}>
+        <Arrow
+          orientation="left"
+          primary
+          style={{ marginRight: '4px' }}
+        />
+        Back
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageBackButton

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPageBackButton/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageBackButton",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageBackButton.tsx"
+}

+ 44 - 0
src/components/modules/SetupModule/ui/SetupPageInputWrapper/SetupPageInputWrapper.tsx

@@ -0,0 +1,44 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+
+const Wrapper = styled.div`
+  margin-top: 16px;
+`
+const Label = styled.div`
+  text-transform: uppercase;
+  font-size: 9px;
+`
+const Input = styled.div``
+type Props = {
+  label: string
+  style?: React.CSSProperties
+}
+
+@observer
+class SetupPageInputWrapper extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper>
+        <Label>{this.props.label}</Label>
+        <Input>{this.props.children}</Input>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageInputWrapper

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPageInputWrapper/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageInputWrapper",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageInputWrapper.tsx"
+}

+ 86 - 0
src/components/modules/SetupModule/ui/SetupPageLicenceInput/SetupPageLicenceInput.tsx

@@ -0,0 +1,86 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+import { SetupPageLicenceType } from '@src/@types/InitialSetup'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
+import licenceImage from './resources/licenceImage.svg'
+import trialLicenceImage from './resources/trialLicenceImage.svg'
+
+const Wrapper = styled.div`
+  display: flex;
+`
+const getLicenceTypeBackground = (type: SetupPageLicenceType) => (type === 'paid' ? '#bdc2d0' : '#f1d5dc')
+
+const ButtonInput = styled.div<{
+  selected?: boolean,
+  type: SetupPageLicenceType
+}>`
+  background: ${props => getLicenceTypeBackground(props.type)};
+  border-radius: 4px;
+  color: ${ThemePalette.black};
+  border: 2px solid ${props => (props.selected ? ThemePalette.primary : getLicenceTypeBackground(props.type))};
+  padding: 5px;
+  cursor: pointer;
+  font-size: 12px;
+  text-align: center;
+  margin-right: 16px;
+  width: 100px;
+  transition: all ${ThemeProps.animations.swift};
+`
+const Image = css`
+  height: 48px;
+`
+const LicenceImage = styled.img`
+  ${Image}
+`
+const TrialLicenceImage = styled.img`
+  ${Image}
+`
+const Label = styled.div``
+type Props = {
+  licenceType: SetupPageLicenceType
+  onLicenceTypeChange: (type: SetupPageLicenceType) => void
+  style?: React.CSSProperties
+}
+
+@observer
+class SetupPageLicenceInput extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper style={this.props.style}>
+        <ButtonInput
+          selected={this.props.licenceType === 'trial'}
+          onClick={() => { this.props.onLicenceTypeChange('trial') }}
+          type="trial"
+        >
+          <TrialLicenceImage src={trialLicenceImage} />
+          <Label>Trial Licence</Label>
+        </ButtonInput>
+        <ButtonInput
+          selected={this.props.licenceType === 'paid'}
+          onClick={() => { this.props.onLicenceTypeChange('paid') }}
+          type="paid"
+        >
+          <LicenceImage src={licenceImage} />
+          <Label>Paid Licence</Label>
+        </ButtonInput>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageLicenceInput

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPageLicenceInput/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageLicenceInput",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageLicenceInput.tsx"
+}

+ 8 - 0
src/components/modules/SetupModule/ui/SetupPageLicenceInput/resources/licenceImage.svg

@@ -0,0 +1,8 @@
+<svg width="68" height="95" xmlns="http://www.w3.org/2000/svg">
+  <g stroke-width="1.5" stroke="#0044CB" fill="none" fill-rule="evenodd">
+    <path d="M11.256 1h45.488c3.567 0 4.86.371 6.163 1.069a7.27 7.27 0 0 1 3.024 3.024C66.63 6.396 67 7.689 67 11.256v71.837c0 3.566-.371 4.86-1.069 6.163a7.27 7.27 0 0 1-3.024 3.024c-1.303.697-2.596 1.068-6.163 1.068H11.256c-3.567 0-4.86-.37-6.163-1.068a7.27 7.27 0 0 1-3.024-3.024C1.37 87.952 1 86.659 1 83.093V11.256c0-3.567.371-4.86 1.069-6.163a7.27 7.27 0 0 1 3.024-3.024C6.396 1.37 7.689 1 11.256 1zM12 16.379h34.524M12 24.379h28.31M12 33.379h43.5M12 41.379h35.214M12 49.379h17.262"/>
+    <path d="m49.44 62-.137.001c-3.388.062-7.584 2.358-10.949 5.993-5.162 5.575-6.893 12.58-3.858 15.616.937.937 2.279 1.42 3.873 1.388 3.388-.062 7.583-2.358 10.948-5.993 5.163-5.574 6.893-12.58 3.859-15.615-.91-.911-2.2-1.39-3.736-1.39"/>
+    <path d="M43.916 69C36.243 69 30 71.09 30 73.659c0 2.568 6.243 4.658 13.916 4.658 7.674 0 13.917-2.09 13.917-4.658 0-2.57-6.243-4.659-13.917-4.659"/>
+    <path d="M38.231 62c-1.536 0-2.825.479-3.735 1.39-3.035 3.036-1.304 10.04 3.857 15.615 3.366 3.635 7.562 5.931 10.95 5.993 1.597.025 2.936-.45 3.873-1.388 3.034-3.036 1.304-10.041-3.859-15.616-3.365-3.635-7.56-5.93-10.948-5.993L38.23 62"/>
+  </g>
+</svg>

Diferenças do arquivo suprimidas por serem muito extensas
+ 9 - 0
src/components/modules/SetupModule/ui/SetupPageLicenceInput/resources/trialLicenceImage.svg


+ 77 - 0
src/components/modules/SetupModule/ui/SetupPagePasswordStrength/SetupPagePasswordStrength.tsx

@@ -0,0 +1,77 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+import taiPasswordStrength from 'tai-password-strength'
+import { ThemePalette, ThemeProps } from '@src/components/Theme'
+
+const Wrapper = styled.div`
+  display: flex;
+  margin-left: -2px;
+`
+type Status = 'VERY_WEAK' | 'WEAK' | 'REASONABLE' | 'STRONG' | 'VERY_STRONG'
+const colorForStatus = (status: Status) => {
+  switch (status) {
+    case 'WEAK':
+      return ThemePalette.alert
+    case 'REASONABLE':
+      return ThemePalette.warning
+    case 'STRONG':
+      return '#758400'
+    case 'VERY_STRONG':
+      return 'green'
+    default:
+      return 'gray'
+  }
+}
+const Bar = styled.div<{ status: Status }>`
+  height: 4px;
+  flex-grow: 1;
+  background: ${props => colorForStatus(props.status)};
+  border: 1px solid rgba(255,255,255,0.2);
+  margin-left: 2px;
+  transition: all ${ThemeProps.animations.swift};
+`
+type Props = {
+  value: string
+  style?: React.CSSProperties
+}
+
+@observer
+class SetupPagePasswordStrength extends React.Component<Props> {
+  render() {
+    const strengthTester = new taiPasswordStrength.PasswordStrength()
+    strengthTester.addCommonPasswords(taiPasswordStrength.commonPasswords)
+    strengthTester.addTrigraphMap(taiPasswordStrength.trigraphs)
+    let strengthCode: Status = strengthTester.check(this.props.value).strengthCode
+    const STRENGTH_CODES: Status[] = ['VERY_WEAK', 'WEAK', 'REASONABLE', 'STRONG', 'VERY_STRONG']
+    let strengthCodeIndex = STRENGTH_CODES.indexOf(strengthCode)
+    if (strengthCode === 'VERY_WEAK') {
+      strengthCode = 'WEAK'
+      strengthCodeIndex = 1
+    }
+    return (
+      <Wrapper style={this.props.style}>
+        <Bar status={strengthCode} />
+        <Bar status={strengthCodeIndex > 1 ? strengthCode : STRENGTH_CODES[0]} />
+        <Bar status={strengthCodeIndex > 2 ? strengthCode : STRENGTH_CODES[0]} />
+        <Bar status={strengthCodeIndex > 3 ? strengthCode : STRENGTH_CODES[0]} />
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPagePasswordStrength

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPagePasswordStrength/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPagePasswordStrength",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPagePasswordStrength.tsx"
+}

+ 58 - 0
src/components/modules/SetupModule/ui/SetupPageServerError/SetupPageServerError.tsx

@@ -0,0 +1,58 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+
+import errorIcon from './resources/error.svg'
+
+const Wrapper = styled.div<{ showBackground?: boolean }>`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 4px;
+  margin-bottom: 16px;
+  ${props => (props.showBackground ? css`background: rgba(255, 0, 0, 0.1);` : '')}
+  border-radius: 4px;
+  padding: 8px;
+`
+const ServerErrorIcon = styled.div`
+  width: 26px;
+  height: 26px;
+  margin-bottom: 4px;
+  background-image: url('${errorIcon}');
+`
+const ServerErrorText = styled.div`
+  margin-top: 4px;
+  text-align: center;
+`
+type Props = {
+  showBackground?: boolean
+  style?: React.CSSProperties
+}
+
+@observer
+class SetupPageServerError extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper style={this.props.style} showBackground={this.props.showBackground}>
+        <ServerErrorIcon />
+        <ServerErrorText>{this.props.children}</ServerErrorText>
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageServerError

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPageServerError/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageServerError",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageServerError.tsx"
+}

+ 14 - 0
src/components/modules/SetupModule/ui/SetupPageServerError/resources/error.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="01-Login-B-Copy-2" transform="translate(-707.000000, -483.000000)" stroke="#F91661">
+            <g id="Icon/Error/Red-32" transform="translate(704.000000, 480.000000)">
+                <g id="Group" transform="translate(4.000000, 4.000000)">
+                    <circle id="Oval-2" cx="12" cy="12" r="12"></circle>
+                    <path d="M17.1428571,6.85714286 L6.85714286,17.1428571" id="Line"></path>
+                    <path d="M17.1428571,17.1428571 L6.85714286,6.85714286" id="Line-Copy"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 39 - 0
src/components/modules/SetupModule/ui/SetupPageTitle/SetupPageTitle.tsx

@@ -0,0 +1,39 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import * as React from 'react'
+import { observer } from 'mobx-react'
+import styled from 'styled-components'
+
+const Wrapper = styled.div`
+  font-size: 20px;
+  text-align: center;
+  margin-bottom: 24px;
+`
+type Props = {
+  title: string
+}
+
+@observer
+class SetupPageTitle extends React.Component<Props> {
+  render() {
+    return (
+      <Wrapper>
+        {this.props.title}
+      </Wrapper>
+    )
+  }
+}
+
+export default SetupPageTitle

+ 6 - 0
src/components/modules/SetupModule/ui/SetupPageTitle/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPageTitle",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPageTitle.tsx"
+}

+ 2 - 2
src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx

@@ -49,7 +49,7 @@ import networkStore from '@src/stores/NetworkStore'
 import { ProviderTypes } from '@src/@types/Providers'
 import minionPoolStore from '@src/stores/MinionPoolStore'
 import LoadingButton from '@src/components/ui/LoadingButton'
-import migrationArrowImage from './images/migration'
+import transferItemIcon from './images/transferItemIcon'
 
 const Wrapper = styled.div<any>`
   ${ThemeProps.exactWidth(`${parseInt(ThemeProps.contentWidth, 10) + 64}px`)}
@@ -592,7 +592,7 @@ class WizardPageContent extends React.Component<Props, State> {
           <WizardTypeIcon
             dangerouslySetInnerHTML={{
               __html: this.props.type === 'replica'
-                ? migrationArrowImage(ThemePalette.alert) : migrationArrowImage(ThemePalette.primary),
+                ? transferItemIcon(ThemePalette.alert) : transferItemIcon(ThemePalette.primary),
             }}
           />
           <EndpointLogos height={32} endpoint={targetEndpoint} />

+ 1 - 3
src/components/modules/WizardModule/WizardPageContent/images/migration.ts → src/components/modules/WizardModule/WizardPageContent/images/transferItemIcon.ts

@@ -12,7 +12,7 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-const image = (color: string) => `
+export default (color: string) => `
 <?xml version="1.0" encoding="UTF-8"?>
 <svg width="59px" height="22px" viewBox="0 0 59 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
@@ -33,5 +33,3 @@ const image = (color: string) => `
     </g>
 </svg>
 `
-
-export default image

+ 373 - 0
src/components/smart/SetupPage/SetupPage.tsx

@@ -0,0 +1,373 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from 'react'
+import styled, { css } from 'styled-components'
+import { observer } from 'mobx-react'
+
+import backgroundImage from '@src/components/smart/LoginPage/images/star-bg.jpg'
+import configLoader from '@src/utils/Config'
+import { CustomerInfoBasic, CustomerInfoTrial, SetupPageLicenceType } from '@src/@types/InitialSetup'
+import setupStore from '@src/stores/SetupStore'
+import notificationStore from '@src/stores/NotificationStore'
+import SetupPageModuleWrapper from '@src/components/modules/SetupModule/SetupPageModuleWrapper'
+import LoadingButton from '@src/components/ui/LoadingButton'
+import Button from '@src/components/ui/Button'
+import SetupPageBackButton from '@src/components/modules/SetupModule/ui/SetupPageBackButton'
+import SetupPageLicence from '@src/components/modules/SetupModule/SetupPageLicence'
+import SetupPageLegal from '@src/components/modules/SetupModule/SetupPageLegal'
+import SetupPageHelp from '@src/components/modules/SetupModule/SetupPageHelp'
+import Logo from '@src/components/ui/Logo'
+import EmptyTemplate from '@src/components/modules/TemplateModule/EmptyTemplate'
+import SetupPageEmailBody from '@src/components/modules/SetupModule/SetupPageEmailBody'
+import cbsImage from '@src/components/modules/SetupModule/resources/cbsl-logo.svg'
+import SetupPageWelcome from '@src/components/modules/SetupModule/SetupPageWelcome'
+
+const Wrapper = styled.div`
+  background-image: url('${backgroundImage}');
+  background-color: transparent;
+  background-size: cover;
+  background-position: center center;
+  background-repeat: no-repeat;
+  position: absolute;
+  overflow: auto;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+`
+const Content = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  height: 100%;
+`
+const Top = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-top: 32px;
+  flex-shrink: 0;
+`
+const Footer = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  flex-shrink: 0;
+  padding: 48px 0;
+`
+const FooterText = styled.div`
+  font-size: 12px;
+  color: white;
+  margin-bottom: 16px;
+`
+const CbsLogo = styled.a`
+  width: 128px;
+  height: 32px;
+  background: url('${cbsImage}') center no-repeat;
+  cursor: pointer;
+`
+const ActionWrapper = css`
+  display: flex;
+  width: 100%;
+`
+const NextOnlyActionWrapper = styled.div`
+  ${ActionWrapper}
+  justify-content: flex-end;
+`
+const BackNextActionWrapper = styled.div`
+  ${ActionWrapper}
+  align-items: center;
+  justify-content: space-between;
+`
+const ModuleWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  > div {
+    margin-top: 16px;
+  }
+`
+type PageName = 'welcome/password' | 'licence' | 'legal/help' | 'email/error'
+// const SETUP_PAGES: PageName[] = ['welcome/password', 'licence', 'legal/help', 'email/error']
+const SETUP_PAGES: PageName[] = ['legal/help']
+
+type Props = {
+  history: any,
+}
+
+type State = {
+  currentPage: number
+  customerInfoBasic: CustomerInfoBasic
+  customerInfoTrial: CustomerInfoTrial
+  highlightEmptyFields: boolean
+  highlightEmail: boolean
+  licenceType: SetupPageLicenceType
+  submitDisabled: boolean
+  submitting: boolean
+}
+
+@observer
+class SetupPage extends React.Component<Props, State> {
+  state = {
+    currentPage: 0,
+    customerInfoBasic: {
+      fullName: '',
+      email: '',
+      company: '',
+      country: '',
+    },
+    customerInfoTrial: {
+      sourcePlatform: null,
+      destinationPlatform: null,
+      interestedIn: 'migrations',
+    },
+    highlightEmptyFields: false,
+    highlightEmail: false,
+    // licenceType: 'trial',
+    licenceType: 'paid',
+    submitDisabled: true,
+    submitting: false,
+  } as State
+
+  scrollableElement: HTMLElement | null = null
+
+  componentDidMount() {
+    setupStore.loadApplianceId()
+  }
+
+  componentDidUpdate(_: Props, prevState: State) {
+    if (prevState.currentPage !== this.state.currentPage) {
+      this.scrollableElement?.scrollTo(0, 0)
+    }
+  }
+
+  handleCustomerInfoBasicChange(field: keyof CustomerInfoBasic, value: any) {
+    const newCustomerInfo: any = this.state.customerInfoBasic
+    newCustomerInfo[field] = value
+    this.setState(() => ({ customerInfoBasic: newCustomerInfo }))
+  }
+
+  handleCustomerInfoTrialChange(field: keyof CustomerInfoTrial, value: any) {
+    const newCustomerInfo: any = this.state.customerInfoTrial
+    newCustomerInfo[field] = value
+    this.setState(() => ({ customerInfoTrial: newCustomerInfo }))
+  }
+
+  handleLegalChange(accepted: boolean) {
+    this.setState({ submitDisabled: !accepted })
+  }
+
+  // async handleUpdatePassword() {
+  //   this.setState({ showPasswordLoading: true })
+  //   try {
+  //     await ObjectUtils.waitFor(() => !setupStore.loadingApplianceId)
+  //     await configLoader.setNotFirstLaunch()
+  //     this.handleNextPage()
+  //   } finally {
+  //     this.setState({ showPasswordLoading: false })
+  //   }
+  // }
+
+  handleNextPage() {
+    this.setState(prevState => ({
+      currentPage: prevState.currentPage + 1,
+      submitDisabled: true,
+    }))
+  }
+
+  handleBackPage() {
+    this.setState(prevState => ({
+      currentPage: prevState.currentPage - 1,
+      submitDisabled: true,
+    }))
+  }
+
+  handleValidateLicenceForm() {
+    if (!this.validateLicenceInputs()) {
+      return
+    }
+    this.handleNextPage()
+  }
+
+  // async handleSubmit() {
+  //   this.setState({ submitting: true })
+  //   try {
+  //     if (this.state.licenceType === 'trial') {
+  //       await setupStore.sendLicenceRequest('trial', { ...this.state.customerInfoBasic, ...this.state.customerInfoTrial })
+  //     } else {
+  //       await setupStore.sendLicenceRequest('paid', { ...this.state.customerInfoBasic })
+  //     }
+  //     this.props.history.push('/login')
+  //   } catch (err) {
+  //     this.setState({ submitting: false })
+  //     this.handleNextPage()
+  //   }
+  // }
+
+  async handleGoToLogin() {
+    this.setState({ submitting: true })
+    await configLoader.setNotFirstLaunch()
+    this.props.history.push('/login')
+    this.setState({ submitting: false })
+  }
+
+  validateLicenceInputs() {
+    const customerInfo = this.state.customerInfoBasic
+    if (!customerInfo.fullName || !customerInfo.email || !customerInfo.company || !customerInfo.country) {
+      notificationStore.alert('Please fill all the required fields', 'error')
+      this.setState({ highlightEmptyFields: true, highlightEmail: false })
+      return false
+    }
+    const emailExp = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\],;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
+    if (!emailExp.test(customerInfo.email)) {
+      notificationStore.alert('Please fill in a correct email format', 'error')
+      this.setState({ highlightEmail: true })
+      return false
+    }
+    this.setState({ highlightEmail: false })
+    return true
+  }
+
+  renderCurrentPage() {
+    const renderModule = (actions: React.ReactNode, content: React.ReactNode) => (
+      <SetupPageModuleWrapper actions={actions}>
+        {content}
+      </SetupPageModuleWrapper>
+    )
+    const currentPageName = SETUP_PAGES[this.state.currentPage]
+    const BUTTON_WIDTH = '132px'
+    switch (currentPageName) {
+      case 'licence':
+        return renderModule(
+          (
+            <BackNextActionWrapper>
+              <SetupPageBackButton onClick={() => { this.handleBackPage() }} />
+              <Button
+                width={BUTTON_WIDTH}
+                onClick={() => { this.handleValidateLicenceForm() }}
+              >
+                Next
+              </Button>
+            </BackNextActionWrapper>
+          ),
+          (
+            <SetupPageLicence
+              customerInfo={this.state.customerInfoBasic}
+              onUpdateCustomerInfo={(field, value) => { this.handleCustomerInfoBasicChange(field, value) }}
+              highlightEmptyFields={this.state.highlightEmptyFields}
+              highlightEmail={this.state.highlightEmail}
+              licenceType={this.state.licenceType}
+              onLicenceTypeChange={licenceType => { this.setState({ licenceType }) }}
+              onSubmit={() => { this.handleValidateLicenceForm() }}
+            />
+          ),
+        )
+      case 'legal/help':
+        return renderModule(
+          (
+            <NextOnlyActionWrapper>
+              {setupStore.sendingLicenceRequest || this.state.submitting ? (
+                <LoadingButton width={BUTTON_WIDTH}>Redirecting...</LoadingButton>
+              ) : (
+                <Button
+                  width={BUTTON_WIDTH}
+                  onClick={() => { this.handleGoToLogin() }}
+                  disabled={this.state.submitDisabled}
+                >
+                  Submit
+                </Button>
+              )}
+            </NextOnlyActionWrapper>
+            // <BackNextActionWrapper>
+            //   <SetupPageBackButton onClick={() => { this.handleBackPage() }} />
+            //   {setupStore.sendingLicenceRequest || this.state.submitting ? (
+            //     <LoadingButton width={BUTTON_WIDTH}>Sending...</LoadingButton>
+            //   ) : (
+            //     <Button
+            //       width={BUTTON_WIDTH}
+            //       onClick={() => { this.handleSubmit() }}
+            //       disabled={this.state.submitDisabled}
+            //     >
+            //       Submit
+            //     </Button>
+            //   )}
+            // </BackNextActionWrapper>
+          ),
+          (
+            <ModuleWrapper>
+              <SetupPageWelcome />
+              <SetupPageLegal
+                licenceType={this.state.licenceType}
+                customerInfoTrial={this.state.customerInfoTrial}
+                onCustomerInfoChange={(f, v) => { this.handleCustomerInfoTrialChange(f, v) }}
+                onLegalChange={a => { this.handleLegalChange(a) }}
+              />
+              <SetupPageHelp style={{ marginTop: '32px' }} />
+            </ModuleWrapper>
+          ),
+        )
+      case 'email/error':
+        return renderModule(
+          (
+            <BackNextActionWrapper>
+              <SetupPageBackButton onClick={() => { this.handleBackPage() }} />
+              {this.state.submitting ? (
+                <LoadingButton width={BUTTON_WIDTH}>Redirecting...</LoadingButton>
+              ) : (
+                <Button
+                  width={BUTTON_WIDTH}
+                  onClick={() => { this.handleGoToLogin() }}
+                >
+                  Go to Login
+                </Button>
+              )}
+            </BackNextActionWrapper>
+          ),
+          (
+            <ModuleWrapper>
+              <SetupPageEmailBody
+                customerInfoBasic={this.state.customerInfoBasic}
+                customerInfoTrial={this.state.customerInfoTrial}
+                licenceType={this.state.licenceType}
+                applianceId={setupStore.applianceId}
+              />
+            </ModuleWrapper>
+          ),
+        )
+      default:
+        return null
+    }
+  }
+
+  render() {
+    return (
+      <EmptyTemplate>
+        <Wrapper ref={ref => { this.scrollableElement = ref }}>
+          <Content>
+            <Top>
+              <Logo />
+              {this.renderCurrentPage()}
+            </Top>
+            <Footer>
+              <FooterText>Coriolis® is a service created by</FooterText>
+              <CbsLogo href="https://cloudbase.it" target="_blank" />
+            </Footer>
+          </Content>
+        </Wrapper>
+      </EmptyTemplate>
+    )
+  }
+}
+
+export default SetupPage

+ 6 - 0
src/components/smart/SetupPage/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "SetupPage",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./SetupPage.tsx"
+}

+ 1 - 0
src/components/ui/Arrow/Arrow.tsx

@@ -47,6 +47,7 @@ type Props = {
   disabled?: boolean,
   color?: string,
   thick?: boolean,
+  style?: React.CSSProperties
 }
 
 @observer

+ 1 - 1
src/components/ui/CopyButton/CopyButton.tsx

@@ -30,7 +30,7 @@ const Wrapper = styled.span`
 `
 
 @observer
-class CopyButton extends React.Component<{}> {
+class CopyButton extends React.Component<any> {
   render() {
     return (
       // eslint-disable-next-line react/jsx-props-no-spreading

+ 4 - 1
src/components/ui/Dropdowns/Dropdown/Dropdown.tsx

@@ -290,6 +290,7 @@ type Props = {
   centered?: boolean,
   useBold?: boolean,
   primary?: boolean
+  labelRenderer?: (item: any, index: number) => React.ReactNode
 }
 type State = {
   showDropdownList: boolean,
@@ -605,6 +606,8 @@ class Dropdown extends React.Component<Props, State> {
               const duplicatedLabel = duplicatedLabels.find(l => l === label)
               const multipleSelected = this.props.selectedItems && this.props.selectedItems
                 .find(j => this.getValue(j) === value)
+              const labelRenderer = this.props.labelRenderer ? this.props.labelRenderer(item, i) : label
+
               const listItem = (
                 <ListItem
                   ref={(ref: HTMLElement | null | undefined) => {
@@ -631,7 +634,7 @@ class Dropdown extends React.Component<Props, State> {
                     />
                   ) : null}
                   <Labels>
-                    {label === '' ? '\u00A0' : label}
+                    {label === '' ? '\u00A0' : labelRenderer}
                     {item.subtitleLabel ? (
                       <SubtitleLabel>{item.subtitleLabel}</SubtitleLabel>
                     ) : null}

+ 17 - 13
src/components/ui/Dropdowns/UserDropdown/UserDropdown.tsx

@@ -19,13 +19,13 @@ import styled, { css } from 'styled-components'
 import autobind from 'autobind-decorator'
 
 import { ThemePalette, ThemeProps } from '@src/components/Theme'
-import { navigationMenu } from '@src/constants'
+import { LEGAL_URLS, navigationMenu } from '@src/constants'
 import type { User } from '@src/@types/User'
 
 import configLoader from '@src/utils/Config'
+import OpenInNewIcon from '@src/components/ui/OpenInNewIcon'
 import userImage from './images/user.svg'
 import userWhiteImage from './images/user-white.svg'
-import openInNewImage from './images/openInNewImage'
 
 const Wrapper = styled.div<any>`
   position: relative;
@@ -42,16 +42,10 @@ const Icon = styled.div<any>`
     opacity: 0.8;
   }
 `
-const Help = styled.div`
+const FlexAlign = styled.div`
   display: flex;
   align-items: center;
 `
-const OpenInNewIcon = styled.div`
-  ${ThemeProps.exactSize('16px')}
-  position: relative;
-  top: -2px;
-  transform: scale(0.6);
-`
 const List = styled.div<any>`
   background: ${ThemePalette.grayscale[1]};
   border-radius: ${ThemeProps.borderRadius};
@@ -67,7 +61,12 @@ const List = styled.div<any>`
 const ListItem = styled.div<any>`
   padding-top: 8px;
 `
-
+const OpenInNewIconWrapper = styled.div`
+  ${ThemeProps.exactSize('16px')}
+  position: relative;
+  top: -2px;
+  transform: scale(0.6);
+`
 const Label = styled.div<{ selectable?: boolean, hoverColor?: string }>`
   display: inline-block;
   white-space: nowrap;
@@ -84,6 +83,7 @@ const Label = styled.div<{ selectable?: boolean, hoverColor?: string }>`
 
 const ListHeader = styled.div<any>`
   position: relative;
+  margin-bottom: 4px;
 
   &:after {
     content: ' ';
@@ -150,6 +150,10 @@ class UserDropdown extends React.Component<Props, State> {
     if (item.value === 'help') {
       window.open('https://cloudbase.it/coriolis-overview/', '_blank')
     }
+
+    if (item.value === 'eula') {
+      window.open(LEGAL_URLS.eula, '_blank')
+    }
   }
 
   @autobind
@@ -210,10 +214,10 @@ class UserDropdown extends React.Component<Props, State> {
       },
       {
         label: (
-          <Help>
+          <FlexAlign>
             Help
-            <OpenInNewIcon dangerouslySetInnerHTML={{ __html: openInNewImage() }} />
-          </Help>
+            <OpenInNewIconWrapper dangerouslySetInnerHTML={{ __html: OpenInNewIcon() }} />
+          </FlexAlign>
         ),
         value: 'help',
       },

+ 1 - 0
src/components/ui/LoadingButton/LoadingButton.tsx

@@ -39,6 +39,7 @@ type Props = {
   large?: boolean
   onClick?: () => void
   style?: React.CSSProperties
+  width?: string
 }
 @observer
 class LoadingButton extends React.Component<Props> {

+ 2 - 2
src/components/ui/Dropdowns/UserDropdown/images/openInNewImage.ts → src/components/ui/OpenInNewIcon/OpenInNewIcon.ts

@@ -1,9 +1,9 @@
-export default () => `
+export default (fill?: string) => `
 <svg xmlns="http://www.w3.org/2000/svg"
   height="24px"
   viewBox="0 0 24 24"
   width="24px"
-  fill="#202134"
+  fill="${fill || '#202134'}"
 >
   <path d="M0 0h24v24H0V0z" fill="none" />
   <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>

+ 6 - 0
src/components/ui/OpenInNewIcon/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "OpenInNewIcon",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./OpenInNewIcon.ts"
+}

+ 7 - 6
src/components/ui/RadioInput/RadioInput.tsx

@@ -52,11 +52,11 @@ const InputStyled = styled.input`
 `
 
 type Props = {
-  label: string,
+  label: React.ReactNode,
   disabledLoading?: boolean,
   disabled?: boolean,
-  checked: boolean,
-  onChange: (checked: boolean) => void,
+  checked?: boolean,
+  onChange?: (checked: boolean) => void,
 }
 @observer
 class RadioInput extends React.Component<Props> {
@@ -65,8 +65,9 @@ class RadioInput extends React.Component<Props> {
       return
     }
     evt.preventDefault()
-
-    this.props.onChange(true)
+    if (this.props.onChange) {
+      this.props.onChange(true)
+    }
   }
 
   render() {
@@ -86,7 +87,7 @@ class RadioInput extends React.Component<Props> {
             {...props}
             disabled={disabled}
             data-test-id="radioInput-input"
-            onChange={e => { this.props.onChange(e.target.checked) }}
+            onChange={e => { if (this.props.onChange) this.props.onChange(e.target.checked) }}
           />
           <Text data-test-id="radioInput-label">{this.props.label}</Text>
         </LabelStyled>

+ 1 - 0
src/components/ui/TextInput/TextInput.tsx

@@ -111,6 +111,7 @@ type Props = {
   onInputKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void,
   onFocus?: () => void,
   onBlur?: () => void,
+  autoComplete?: string
 }
 const TextInput = (props: Props) => {
   const {

+ 5 - 0
src/constants.ts

@@ -107,3 +107,8 @@ export const wizardPages: WizardPage[] = [
 ]
 
 export const basename = process.env.PUBLIC_PATH
+
+export const LEGAL_URLS = {
+  eula: 'https://cloudbase.it/coriolis-eula/',
+  privacy: ' https://cloudbase.it/privacy/',
+}

+ 14 - 4
src/sources/LincenceSource.ts

@@ -18,15 +18,25 @@ import configLoader from '@src/utils/Config'
 import type { Licence, LicenceServerStatus } from '@src/@types/Licence'
 
 class LicenceSource {
-  async loadAppliancesIds(skipLog?: boolean | null): Promise<string[]> {
+  async loadAppliancesIds(config?: {
+    skipLog?: boolean,
+    quietError?: boolean
+  }): Promise<string[]> {
     const url = `${configLoader.config.servicesUrls.coriolisLicensing}/appliances`
-    const response = await Api.send({ url, quietError: true, skipLog })
+    const response = await Api.send({
+      url,
+      quietError: config?.quietError,
+      skipLog: config?.skipLog,
+    })
     return response.data.appliances.map((a: any) => a.id)
   }
 
-  async loadLicenceServerStatus(skipLog?: boolean | null): Promise<LicenceServerStatus> {
+  async loadLicenceServerStatus(config?: {
+    skipLog?: boolean,
+    quietError?: boolean
+  }): Promise<LicenceServerStatus> {
     const url = `${configLoader.config.servicesUrls.coriolisLicensing}/status`
-    const response = await Api.send({ url, quietError: true, skipLog })
+    const response = await Api.send({ url, quietError: config?.quietError, skipLog: config?.skipLog })
     const status: LicenceServerStatus = response.data.status
     status.supported_licence_versions.sort((a, b) => b.localeCompare(a))
     return response.data.status

+ 2 - 2
src/stores/LicenceStore.ts

@@ -34,7 +34,7 @@ class LicenceStore {
       this.loadingLicenceInfo = true
     }
     try {
-      const ids = await licenceSource.loadAppliancesIds(opts?.skipLog)
+      const ids = await licenceSource.loadAppliancesIds({ skipLog: opts?.skipLog, quietError: true })
       if (!ids.length || ids.length > 1) {
         runInAction(() => {
           if (ids.length > 1) {
@@ -46,7 +46,7 @@ class LicenceStore {
       }
       const applianceId = ids[0]
       const [licenceServerStatus, licenceInfo] = await Promise.all([
-        licenceSource.loadLicenceServerStatus(opts?.skipLog),
+        licenceSource.loadLicenceServerStatus({ skipLog: opts?.skipLog, quietError: true }),
         licenceSource.loadLicenceInfo(applianceId, opts?.skipLog),
       ])
       runInAction(() => {

+ 112 - 0
src/stores/SetupStore.ts

@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2021  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import { observable, runInAction, action } from 'mobx'
+import { ProviderTypes } from '../@types/Providers'
+import {
+  CustomerInfoBasic, CustomerInfoFull, CustomerInfoTrial, isCustomerInfoFull, SetupPageLicenceType,
+} from '../@types/InitialSetup'
+import lincenceSource from '../sources/LincenceSource'
+import configLoader from '../utils/Config'
+import ObjectUtils from '../utils/ObjectUtils'
+
+export const customerInfoSetupStoreValueToString = (property: keyof CustomerInfoTrial, value: string | null): string => {
+  switch (property) {
+    case 'interestedIn':
+      return value === 'both' ? 'migrations and replicas' : (value as string)
+    case 'sourcePlatform':
+    case 'destinationPlatform':
+      return value ? configLoader.config.providerNames[value as ProviderTypes] : 'not chosen'
+    default:
+      return value || 'not chosen'
+  }
+}
+
+class SetupStore {
+  @observable
+    sendingLicenceRequest: boolean = false
+
+  @observable
+    loadingApplianceId: boolean = false
+
+  @observable
+    applianceId: string = ''
+
+  @observable
+    applianceIdError: string = ''
+
+  @action
+  async loadApplianceId() {
+    this.loadingApplianceId = true
+    try {
+      const [ids, status] = await Promise.all([
+        lincenceSource.loadAppliancesIds({ quietError: true }),
+        lincenceSource.loadLicenceServerStatus({ quietError: true }),
+      ])
+      if (!ids.length || ids.length > 1) {
+        runInAction(() => {
+          if (ids.length > 1) {
+            this.applianceIdError = 'There appears to be multiple Coriolis appliances defined within the licensing server. This is most likely due to a deployment error or failed cleanup, so please contact Cloudbase Support with this information to resolve the issue.'
+          }
+        })
+        return
+      }
+      runInAction(() => {
+        this.applianceId = `${ids[0]}-licence${status.supported_licence_versions[0]}`
+      })
+    } catch (err) {
+      this.applianceIdError = 'There was an error while requesting the appliance ID.'
+    } finally {
+      runInAction(() => {
+        this.loadingApplianceId = false
+      })
+    }
+  }
+
+  async sendLicenceRequest(licenceType: 'trial', customerInfo: CustomerInfoFull): Promise<void>
+
+  async sendLicenceRequest(licenceType: 'paid', customerInfo: CustomerInfoBasic): Promise<void>
+
+  async sendLicenceRequest(licenceType: SetupPageLicenceType, customerInfo: CustomerInfoFull | CustomerInfoBasic): Promise<void> {
+    this.sendingLicenceRequest = true
+    const payload: any = {
+      customerInfo,
+      licenceType,
+      applianceId: this.applianceId,
+    }
+    if (isCustomerInfoFull(customerInfo)) {
+      payload.customerInfo = {
+        ...payload.customerInfo,
+        interestedIn: customerInfoSetupStoreValueToString('interestedIn', customerInfo.interestedIn),
+        sourcePlatform: customerInfoSetupStoreValueToString('sourcePlatform', customerInfo.sourcePlatform),
+        destinationPlatform: customerInfoSetupStoreValueToString('destinationPlatform', customerInfo.destinationPlatform),
+      }
+    }
+    try {
+      // await ObjectUtils.retry(() => apiCaller.send({
+      //   url: '/okokok',
+      //   quietError: true,
+      // }))
+      await ObjectUtils.wait(2000)
+      console.log('Sending payload', payload)
+      throw new Error('not implemented')
+    } finally {
+      runInAction(() => {
+        this.sendingLicenceRequest = false
+      })
+    }
+  }
+}
+
+export default new SetupStore()

+ 5 - 1
src/stores/UserStore.ts

@@ -18,6 +18,7 @@ import type { Project } from '@src/@types/Project'
 import UserSource from '@src/sources/UserSource'
 import projectStore from './ProjectStore'
 import notificationStore from './NotificationStore'
+import configLoader from '../utils/Config'
 
 /**
  * This is the authentication / authorization flow:
@@ -72,6 +73,9 @@ class UserStore {
       await this.getLoggedUserInfo()
       await this.loginScoped(this.loggedUser ? this.loggedUser.project_id : '', true)
       await this.isAdmin()
+      // If the user skipped the setup process and has successfully logged in,
+      // make sure the setup page doesn't get displayed again
+      configLoader.setNotFirstLaunch()
       runInAction(() => { this.loggedIn = true })
       notificationStore.alert('Signed in', 'success')
     } catch (err) {
@@ -163,7 +167,7 @@ class UserStore {
       const users = await UserSource.getAllUsers(options?.skipLog, options?.quietError)
       runInAction(() => { this.users = users })
     } catch (err) {
-      if (err.data?.error?.code !== 403) {
+      if ((err as any).data?.error?.code !== 403) {
         throw err
       }
     } finally {

+ 2 - 0
src/utils/ApiCaller.ts

@@ -35,6 +35,7 @@ export type RequestOptions = {
   skipLog?: boolean | null,
   cache?: boolean | null,
   cacheFor?: number | null,
+  timeout?: number
 }
 
 let cancelables: Cancelable[] = []
@@ -81,6 +82,7 @@ class ApiCaller {
       headers: options.headers || {},
       data: options.data || null,
       responseType: options.responseType || 'json',
+      timeout: options.timeout,
     }
 
     if (options.cancelId) {

+ 21 - 1
src/utils/Config.ts

@@ -4,9 +4,29 @@ import apiCaller from './ApiCaller'
 class ConfigLoader {
   config!: Config
 
+  isFirstLaunch!: boolean
+
   async load() {
     const res = await apiCaller.get('/api/config')
-    this.config = res.data
+    this.config = res.data.config
+    this.isFirstLaunch = res.data.isFirstLaunch
+  }
+
+  async setNotFirstLaunch() {
+    await apiCaller.send({
+      url: '/api/config/first-launch',
+      method: 'POST',
+      data: { isFirstLaunch: false },
+    })
+    this.isFirstLaunch = false
+  }
+
+  async setInitialAdminPassword(password: string) {
+    await apiCaller.send({
+      url: '/api/config/admin-password',
+      method: 'POST',
+      data: { password },
+    })
   }
 }
 

+ 23 - 0
src/utils/ObjectUtils.ts

@@ -99,6 +99,29 @@ class ObjectUtils {
   static capitalizeFirstLetter(value: string): string {
     return value.charAt(0).toUpperCase() + value.slice(1)
   }
+
+  static async retry(retryFunction: () => Promise<any>, retryEvery: number = 1000, retryCount: number = 3): Promise<any> {
+    let currentTry = 0
+    const retryLoop = async (): Promise<any> => {
+      try {
+        currentTry += 1
+        if (currentTry > 1) {
+          console.log('Retrying... ', currentTry)
+        }
+        const result = await retryFunction()
+        return result
+      } catch (err) {
+        if (currentTry >= retryCount) {
+          console.error(`Retry failed after ${retryCount} attempts.`)
+          throw err
+        }
+        await this.wait(retryEvery)
+        return retryLoop()
+      }
+    }
+
+    return retryLoop()
+  }
 }
 
 export default ObjectUtils

+ 1 - 0
tsconfig.json

@@ -74,6 +74,7 @@
   "include": [
     "src",
     "server",
+    "./config.ts"
   ],
   "exclude": [
     "src/**/test.tsx"

+ 5 - 0
yarn.lock

@@ -11050,6 +11050,11 @@ symbol.prototype.description@^1.0.0:
     has-symbols "^1.0.2"
     object.getownpropertydescriptors "^2.1.2"
 
+tai-password-strength@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/tai-password-strength/-/tai-password-strength-1.1.2.tgz#746e565cdad9b275e1b159b6f9fea2e969034436"
+  integrity sha512-jNyf/Icia2Tzsi1pZI0RMneZbV+w3kLUmz0pmrHxAWhDANjh9+gbZE2gdWYM3J0ELyxw4AyWTgNRNiXLx2ggDQ==
+
 tapable@^1.0.0, tapable@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff