Преглед изворни кода

Show Setup page when launching for the first time

When the app is launched for the first time a Welcome page is shown with
legal agreements and help links.

The EULA can also be viewed in the app's About modal.

This PR also includes code for trial and paid licence request forms and
subsequent flow steps. That code is temporarily disabled.
Sergiu Miclea пре 4 година
родитељ
комит
d7380b0e59
62 измењених фајлова са 2208 додато и 44 уклоњено
  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"
+}

Разлика између датотеке није приказан због своје велике величине
+ 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"
+}

Разлика између датотеке није приказан због своје велике величине
+ 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>

Разлика између датотеке није приказан због своје велике величине
+ 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"

Неке датотеке нису приказане због велике количине промена