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

View current license information and add new ones

Current license information can be viewed in the About modal.
There you can also add a new licence file by pasting, uploading or
dragging it.

# Note #
There's a new env variable: CORIOLIS_LICENSING_BASE_URL
If this variable is not set the licensing API URL is constructed from
CORIOLIS_URL:`${CORIOLIS_URL}:37667/licensing/v1`
Sergiu Miclea пре 7 година
родитељ
комит
2d9e81fedd

+ 1 - 0
package.json

@@ -65,6 +65,7 @@
     "babel-plugin-transform-decorators-legacy": "^1.3.4",
     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
     "babel-plugin-transform-react-remove-prop-types": "^0.4.9",
+    "babel-polyfill": "^6.26.0",
     "babel-preset-env": "^1.6.0",
     "babel-preset-flow": "^6.23.0",
     "babel-preset-react": "^6.24.1",

+ 4 - 1
server/main.js

@@ -25,13 +25,16 @@ import packageJson from '../package.json'
 const app = express()
 const PORT = process.env.PORT || 3000
 const isDev = process.argv.find(a => a === '--dev')
+const CORIOLIS_URL = process.env.CORIOLIS_URL || '/'
+let CORIOLIS_LICENSING_BASE_URL = process.env.CORIOLIS_LICENSING_BASE_URL || ''
 
 // Write file to disk with process env variables, so that the client code can read
 if (!fs.existsSync('./dist')) {
   fs.mkdirSync('./dist')
 }
 fs.writeFileSync('./dist/env.js', `window.env = {
-  CORIOLIS_URL: '${(process.env.CORIOLIS_URL || '/')}',
+  CORIOLIS_URL: '${CORIOLIS_URL}',
+  CORIOLIS_LICENSING_BASE_URL: '${(CORIOLIS_LICENSING_BASE_URL)}',
   ENV: '${isDev ? 'development' : 'production'}',
 }
 `)

+ 11 - 3
src/components/atoms/TextArea/TextArea.jsx

@@ -60,12 +60,20 @@ const Input = styled.textarea`
     color: ${Palette.grayscale[3]};
   }
 `
-
 @observer
-class TextArea extends React.Component<{}> {
+class TextArea extends React.Component<any> {
   render() {
     return (
-      <Input {...this.props} />
+      <Input
+        {...this.props}
+        innerRef={r => {
+          if (this.props.innerRef) {
+            this.props.innerRef(r)
+          } else if (this.props.customRef) {
+            this.props.customRef(r)
+          }
+        }}
+      />
     )
   }
 }

+ 1 - 1
src/components/organisms/DetailsPageHeader/DetailsPageHeader.jsx

@@ -22,7 +22,7 @@ import { observer } from 'mobx-react'
 import NavigationMini from '../../molecules/NavigationMini'
 import NotificationDropdown from '../../molecules/NotificationDropdown'
 import UserDropdown from '../../molecules/UserDropdown'
-import AboutModal from '../../organisms/AboutModal'
+import AboutModal from '../../pages/AboutModal'
 
 import type { User as UserType } from '../../../types/User'
 

+ 334 - 0
src/components/organisms/Licence/Licence.jsx

@@ -0,0 +1,334 @@
+/*
+Copyright (C) 2019  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/>.
+*/
+
+// @flow
+
+import React from 'react'
+import { observer } from 'mobx-react'
+import styled, { css } from 'styled-components'
+import moment from 'moment'
+
+import Button from '../../atoms/Button'
+import LoadingButton from '../../molecules/LoadingButton'
+import StatusImage from '../../atoms/StatusImage'
+import TextArea from '../../atoms/TextArea'
+
+import StyleProps from '../../styleUtils/StyleProps'
+import Palette from '../../styleUtils/Palette'
+
+import type { Licence } from '../../../types/Licence'
+
+import licenceImage from './images/licence'
+
+const Wrapper = styled.div`
+  min-height: 0;
+  overflow: auto;
+  width: 100%;
+`
+const TextAreaStyled = styled(TextArea)`
+  ${props => props.dropzone ? css`
+    border: 1px dashed ${Palette.primary};
+  ` : ''}
+`
+const LicenceInfoWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  width: 100%;
+  padding: 0 32px;
+`
+const LicenceRow = styled.div`
+  display: flex;
+  margin-top: 16px;
+`
+const LicenceRowLabel = styled.div`
+  display: flex;
+  align-items: center;
+  font-weight: ${StyleProps.fontWeights.medium};
+  font-size: 10px;
+  text-transform: uppercase;
+  color: ${Palette.grayscale[3]};
+`
+const LicenceLink = styled.span`
+  text-transform: initial;
+  color: ${Palette.primary};
+  font-weight: ${StyleProps.fontWeights.regular};
+  cursor: pointer;
+`
+const LicenceRowContent = styled.div`
+  ${props => props.width ? css`width: ${props.width};` : ''}
+`
+const LicenceRowDescription = styled.div`
+  margin-top: 4px;
+`
+const LoadingWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`
+const LoadingText = styled.div`
+  font-size: 18px;
+  margin-top: 32px;
+`
+const ButtonsWrapper = styled.div`
+  display: flex;
+  margin-top: 48px;
+  justify-content: ${props => props.spaceBetween ? 'space-between' : 'center'};
+  padding: 0 32px;
+`
+const Logo = styled.div`
+  width: 96px;
+  height: 96px;
+  margin: 0 auto;
+  text-align: center;
+`
+const LicenceAddWrapper = styled.div`
+  padding: 0 32px;
+`
+const Description = styled.div`
+  color: ${Palette.grayscale[3]};
+`
+const FakeFileInput = styled.input`
+  position: absolute;
+  opacity: 0;
+  top: -99999px;
+`
+
+type Props = {
+  licenceInfo: ?Licence,
+  loadingLicenceInfo: boolean,
+  onRequestClose: () => void,
+  onAddModeChange: (addMode: boolean) => void,
+  addMode: boolean,
+  onAddLicence: (licence: string) => void,
+  addingLicence: boolean,
+}
+type State = {
+  licence: string,
+  isValid: boolean,
+  highlightDropzone: boolean,
+}
+
+const readFromFileList = async (fileList: FileList): Promise<?string> => {
+  if (!fileList.length) {
+    return null
+  }
+  let file = fileList[0]
+  let reader = new FileReader()
+  return new Promise((resolve, reject) => {
+    reader.onload = e => { resolve(e.target.result) }
+    reader.onerror = e => { reject(e) }
+    reader.readAsText(file)
+  })
+}
+
+@observer
+class LicenceC extends React.Component<Props, State> {
+  state = {
+    licence: '',
+    isValid: false,
+    highlightDropzone: false,
+  }
+
+  fileInput: HTMLElement
+
+  dragDropAdded: boolean = false
+
+  addDragAndDrop() {
+    if (this.dragDropAdded) {
+      return
+    }
+
+    window.addEventListener('dragenter', e => {
+      this.setState({ highlightDropzone: true })
+      e.dataTransfer.dropEffect = 'copy'
+      e.preventDefault()
+    })
+    window.addEventListener('dragover', e => {
+      e.dataTransfer.dropEffect = 'copy'
+      e.preventDefault()
+    })
+    window.addEventListener('dragleave', e => {
+      if (!e.clientX && !e.clientY) {
+        this.setState({ highlightDropzone: false })
+      }
+    })
+    window.addEventListener('drop', async e => {
+      e.preventDefault()
+      this.setState({ highlightDropzone: false })
+      let text = await readFromFileList(e.dataTransfer.files)
+      if (text) {
+        this.handleLicenceChange(text)
+      }
+    })
+    this.dragDropAdded = true
+  }
+
+  validate() {
+    let isValid = true
+    if (this.state.licence.indexOf('-----BEGIN CORIOLIS LICENCE-----') !== 0) {
+      isValid = false
+    }
+    if (this.state.licence.indexOf('-----END CORIOLIS LICENCE-----') === -1) {
+      isValid = false
+    }
+    this.setState({ isValid })
+  }
+
+  handleAddLicenceClick() {
+    this.setState({ licence: '' })
+    this.props.onAddModeChange(true)
+    setTimeout(() => { this.addDragAndDrop() }, 1000)
+  }
+
+  handleAddButtonClick() {
+    this.props.onAddLicence(this.state.licence)
+  }
+
+  handleLicenceChange(licence: string) {
+    this.setState({ licence }, () => { this.validate() })
+  }
+
+  handleUploadClick() {
+    this.fileInput.click()
+  }
+
+  async handleFileUpload(files: FileList) {
+    let text = await readFromFileList(files)
+    if (text) {
+      this.handleLicenceChange(text)
+    }
+  }
+
+  renderLicenceInfoLoading() {
+    return (
+      <LoadingWrapper>
+        <StatusImage loading />
+        <LoadingText>Loading licence info ...</LoadingText>
+      </LoadingWrapper>
+    )
+  }
+
+  renderLicenceInfo(info: Licence) {
+    return (
+      <LicenceInfoWrapper>
+        <LicenceRow>
+          <LicenceRowContent>
+            <LicenceRowLabel>
+              Licence
+              <LicenceLink
+                style={{ marginLeft: '8px' }}
+                onClick={() => { this.handleAddLicenceClick() }}
+              >Add Licence</LicenceLink>
+            </LicenceRowLabel>
+            <LicenceRowDescription>
+              Coriolis® Licence is active until
+              &nbsp;{moment(info.currentPeriodEnd).format('DD MMM YYYY')}
+              &nbsp;({moment(info.currentPeriodEnd).diff(new Date(), 'days')} days from now).
+            </LicenceRowDescription>
+          </LicenceRowContent>
+        </LicenceRow>
+        <LicenceRow>
+          <LicenceRowContent width="50%" style={{ marginRight: '32px' }}>
+            <LicenceRowLabel>VM Replicas</LicenceRowLabel>
+            <LicenceRowDescription>
+              {info.totalReplicas - info.performedReplicas} VM Replicas remaining.
+            </LicenceRowDescription>
+          </LicenceRowContent>
+          <LicenceRowContent width="50%">
+            <LicenceRowLabel>VM Migrations</LicenceRowLabel>
+            <LicenceRowDescription>
+              {info.totalMigations - info.performedMigrations} VM Migrations remaining.
+            </LicenceRowDescription>
+          </LicenceRowContent>
+        </LicenceRow>
+      </LicenceInfoWrapper>
+    )
+  }
+
+  renderButtons() {
+    return (
+      <ButtonsWrapper spaceBetween={this.props.addMode}>
+        <Button
+          secondary
+          large
+          onClick={() => { this.props.onRequestClose() }}
+        >Close</Button>
+        {(this.props.addMode && !this.props.addingLicence) ? (
+          <Button
+            large
+            onClick={() => { this.handleAddButtonClick() }}
+            disabled={!this.state.isValid}
+          >Add Licence</Button>
+        ) :
+          (this.props.addMode && this.props.addingLicence) ?
+            <LoadingButton
+              large
+              onClick={() => { this.handleAddButtonClick() }}
+            >Add Licence
+            </LoadingButton>
+            : null}
+      </ButtonsWrapper>
+    )
+  }
+
+  renderLicenceAdd() {
+    return (
+      <LicenceAddWrapper>
+        <Logo
+          dangerouslySetInnerHTML={
+            { __html: licenceImage(this.state.isValid ? Palette.primary : Palette.grayscale[5]) }
+          }
+        />
+        <LicenceRowLabel style={{ marginTop: '32px' }}>Licence</LicenceRowLabel>
+        <TextAreaStyled
+          placeholder="Paste/Drag Licence file here ..."
+          dropzone={this.state.highlightDropzone}
+          style={{
+            width: '100%',
+            marginTop: '4px',
+          }}
+          value={this.state.licence}
+          onChange={e => { this.handleLicenceChange(e.target.value) }}
+        />
+        <Description>
+          Drag the Licence file or paste the contents in box above.
+          <br />Alternatively you can <LicenceLink
+            onClick={() => { this.handleUploadClick() }}
+          >upload</LicenceLink> the file.
+        </Description>
+        <FakeFileInput
+          type="file"
+          innerRef={r => { this.fileInput = r }}
+          accept=".pem, .txt"
+          onChange={e => { this.handleFileUpload(e.target.files) }}
+        />
+      </LicenceAddWrapper>
+    )
+  }
+
+  render() {
+    let showInfo = !this.props.loadingLicenceInfo && !this.props.addMode
+    return (
+      <Wrapper>
+        {showInfo && this.props.licenceInfo ? this.renderLicenceInfo(this.props.licenceInfo) : null}
+        {this.props.addMode ? this.renderLicenceAdd() : null}
+        {this.props.loadingLicenceInfo ? this.renderLicenceInfoLoading() : null}
+        {this.renderButtons()}
+      </Wrapper>
+    )
+  }
+}
+
+export default LicenceC

+ 27 - 0
src/components/organisms/Licence/images/licence.js

@@ -0,0 +1,27 @@
+export default color => `
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="68px" height="95px" viewBox="0 0 68 95" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
+    <title>Main Icon</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Coriolis" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="About/Add-Licence-Validated" transform="translate(-254.000000, -97.000000)" stroke="${color}">
+            <g id="Modal-Connection">
+                <g id="Icon/Licence/96" transform="translate(240.000000, 96.000000)">
+                    <path d="M25.2555408,2 L70.7444592,2 C74.3105342,2 75.6036791,2.37130244 76.9073828,3.06853082 C78.2110865,3.76575919 79.2342408,4.78891348 79.9314692,6.09261719 C80.6286976,7.39632089 81,8.68946584 81,12.2555408 L81,84.092944 C81,87.659019 80.6286976,88.952164 79.9314692,90.2558677 C79.2342408,91.5595714 78.2110865,92.5827257 76.9073828,93.279954 C75.6036791,93.9771824 74.3105342,94.3484848 70.7444592,94.3484848 L25.2555408,94.3484848 C21.6894658,94.3484848 20.3963209,93.9771824 19.0926172,93.279954 C17.7889135,92.5827257 16.7657592,91.5595714 16.0685308,90.2558677 C15.3713024,88.952164 15,87.659019 15,84.092944 L15,12.2555408 C15,8.68946584 15.3713024,7.39632089 16.0685308,6.09261719 C16.7657592,4.78891348 17.7889135,3.76575919 19.0926172,3.06853082 C20.3963209,2.37130244 21.6894658,2 25.2555408,2 Z" id="Rectangle" stroke-width="1.5"></path>
+                    <path d="M26,17.3787879 L60.5238095,17.3787879" id="Stroke-5" stroke-width="1.5"></path>
+                    <path d="M26,25.3787879 L54.3095238,25.3787879" id="Stroke-5-Copy" stroke-width="1.5"></path>
+                    <path d="M26,34.3787879 L69.5,34.3787879" id="Stroke-5-Copy-2" stroke-width="1.5"></path>
+                    <path d="M26,42.3787879 L61.2142857,42.3787879" id="Stroke-5-Copy-3" stroke-width="1.5"></path>
+                    <path d="M26,50.3787879 L43.2619048,50.3787879" id="Stroke-5-Copy-4" stroke-width="1.5"></path>
+                    <g id="Group" stroke-width="1" fill-rule="evenodd" transform="translate(44.000000, 63.000000)">
+                        <path d="M19.4400126,2.85993451e-13 C19.394621,2.85993451e-13 19.3486476,0.000558850024 19.3026741,0.00111770005 C15.9151865,0.0631500527 11.7193808,2.3594648 8.35400703,5.99422536 C3.1915947,11.5687544 1.46089756,18.5739394 4.49572794,21.6096127 C5.43323739,22.5473631 6.77461496,23.0302095 8.3685556,22.998355 C11.7566251,22.9357638 15.9518489,20.6400079 19.3172227,17.0052474 C24.479635,11.4307184 26.2103322,4.42553334 23.1755018,1.38986001 C22.2653436,0.478934471 20.9757589,2.85993451e-13 19.4400126,2.85993451e-13" id="Fill-14" stroke-width="1.5" fill="#FFFFFF"></path>
+                        <path d="M13.9163663,7 C6.24259759,7 -1.73329949e-13,9.08954024 -1.73329949e-13,11.6585738 C-1.73329949e-13,14.2270485 6.24259759,16.3165888 13.9163663,16.3165888 C21.5901349,16.3165888 27.8333333,14.2270485 27.8333333,11.6585738 C27.8333333,9.08954024 21.5901349,7 13.9163663,7" id="Combined-Shape" stroke-width="1.5"></path>
+                        <path d="M8.23123884,1.20792265e-13 C6.69549246,1.20792265e-13 5.40590778,0.478934471 4.49574963,1.38986001 C1.46091925,4.42553334 3.19161639,11.4307184 8.35344678,17.0052474 C11.7194025,20.6400079 15.9146263,22.9363227 19.3026958,22.998355 C20.9001281,23.0229444 22.238596,22.5473631 23.1755235,21.6096127 C26.2103538,18.5739394 24.4796567,11.5687544 19.3172444,5.99422536 C15.9518706,2.3594648 11.7566468,0.0631500527 8.36857729,0.00111770005 C8.32260382,0.000558850024 8.27663036,1.20792265e-13 8.23123884,1.20792265e-13" id="Fill-3" stroke-width="1.5"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
+`

+ 6 - 0
src/components/organisms/Licence/package.json

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

+ 13 - 3
src/components/organisms/PageHeader/PageHeader.jsx

@@ -30,7 +30,7 @@ import ChooseProvider from '../../organisms/ChooseProvider'
 import Endpoint from '../../organisms/Endpoint'
 import UserModal from '../../organisms/UserModal'
 import ProjectModal from '../../organisms/ProjectModal'
-import AboutModal from '../../organisms/AboutModal'
+import AboutModal from '../../pages/AboutModal'
 
 import projectStore from '../../../stores/ProjectStore'
 import userStore from '../../../stores/UserStore'
@@ -112,6 +112,9 @@ class PageHeader extends React.Component<Props, State> {
     switch (item.value) {
       case 'about':
         this.setState({ showAbout: true })
+        if (this.props.onModalOpen) {
+          this.props.onModalOpen()
+        }
         return
       case 'signout':
         userStore.logout()
@@ -225,7 +228,8 @@ class PageHeader extends React.Component<Props, State> {
       this.state.showChooseProviderModal ||
       this.state.showEndpointModal ||
       this.state.showProjectModal ||
-      this.state.showUserModal
+      this.state.showUserModal ||
+      this.state.showAbout
     ) {
       return
     }
@@ -295,7 +299,13 @@ class PageHeader extends React.Component<Props, State> {
           />
         ) : null}
         {this.state.showAbout ? (
-          <AboutModal onRequestClose={() => { this.setState({ showAbout: false }) }} />
+          <AboutModal onRequestClose={() => {
+            this.setState({ showAbout: false })
+            if (this.props.onModalClose) {
+              this.props.onModalClose()
+            }
+          }}
+          />
         ) : null}
       </Wrapper>
     )

+ 47 - 17
src/components/organisms/AboutModal/AboutModal.jsx → src/components/pages/AboutModal/AboutModal.jsx

@@ -21,10 +21,13 @@ import styled from 'styled-components'
 import apiCaller from '../../../utils/ApiCaller'
 import logger from '../../../utils/ApiLogger'
 
-import Button from '../../atoms/Button'
 import Modal from '../../molecules/Modal/Modal'
+import LicenceComponent from '../../organisms/Licence'
 
 import Palette from '../../styleUtils/Palette'
+import StyleProps from '../../styleUtils/StyleProps'
+
+import licenceStore from '../../../stores/LicenceStore'
 
 import logoImage from './images/coriolis-logo.svg'
 
@@ -35,6 +38,8 @@ const Wrapper = styled.div`
   justify-content: center;
   padding: 60px 0 32px 0;
   position: relative;
+  height: 100%;
+  min-height: 0;
 `
 const Gradient = styled.div`
   position: absolute;
@@ -49,10 +54,14 @@ const Content = styled.div`
   display: flex;
   flex-direction: column;
   align-items: center;
+  width: 100%;
+  height: 100%;
+  min-height: 0;
 `
+const AboutContentWrapper = styled.div``
 const Logo = styled.div`
   width: 362px;
-  height: 71px;
+  ${StyleProps.exactHeight('71px')}
   background: url('${logoImage}') center no-repeat;
 `
 const Text = styled.div`
@@ -86,18 +95,27 @@ type Props = {
 
 type State = {
   version: string,
+  licenceAddMode: boolean,
 }
 
 @observer
 class AboutModal extends React.Component<Props, State> {
   state = {
     version: '-',
+    licenceAddMode: false,
   }
 
   componentWillMount() {
     apiCaller.get('/version').then(res => {
       this.setState({ version: res.data.version })
     })
+    licenceStore.loadLicenceInfo()
+  }
+
+  async handleAddLicence(licence: string) {
+    await licenceStore.addLicence(licence)
+    licenceStore.loadLicenceInfo()
+    this.setState({ licenceAddMode: false })
   }
 
   render() {
@@ -108,22 +126,34 @@ class AboutModal extends React.Component<Props, State> {
         onRequestClose={() => { this.props.onRequestClose() }}
       >
         <Wrapper>
-          <Gradient />
+          {!this.state.licenceAddMode ? <Gradient /> : null}
           <Content>
-            <Logo />
-            <Text>
-              <TextLine>
-                <span>Version {this.state.version}</span>
-                <span>|</span>
-                <Link href="https://github.com/cloudbase/coriolis/issues" target="_blank">Report an Issue</Link>
-                <span>|</span>
-                <LinkMock onClick={() => { logger.download() }} >Download Log</LinkMock>
-              </TextLine>
-              <TextLine>
-                © {new Date().getFullYear()} Cloudbase Solutions. All Rights Reserved.
-              </TextLine>
-            </Text>
-            <Button secondary large onClick={() => { this.props.onRequestClose() }}>Close</Button>
+            {!this.state.licenceAddMode ? (
+              <AboutContentWrapper>
+                <Logo />
+                <Text>
+                  <TextLine>
+                    <span>Version {this.state.version}</span>
+                    <span>|</span>
+                    <Link href="https://github.com/cloudbase/coriolis/issues" target="_blank">Report an Issue</Link>
+                    <span>|</span>
+                    <LinkMock onClick={() => { logger.download() }} >Download Log</LinkMock>
+                  </TextLine>
+                  <TextLine>
+                    © {new Date().getFullYear()} Cloudbase Solutions. All Rights Reserved.
+                  </TextLine>
+                </Text>
+              </AboutContentWrapper>
+            ) : null}
+            <LicenceComponent
+              licenceInfo={licenceStore.licenceInfo}
+              loadingLicenceInfo={licenceStore.loadingLicenceInfo}
+              onRequestClose={this.props.onRequestClose}
+              addMode={this.state.licenceAddMode}
+              onAddModeChange={licenceAddMode => { this.setState({ licenceAddMode }) }}
+              onAddLicence={licence => { this.handleAddLicence(licence) }}
+              addingLicence={licenceStore.addingLicence}
+            />
           </Content>
         </Wrapper>
       </Modal>

+ 0 - 0
src/components/organisms/AboutModal/images/coriolis-logo.svg → src/components/pages/AboutModal/images/coriolis-logo.svg


+ 0 - 0
src/components/organisms/AboutModal/package.json → src/components/pages/AboutModal/package.json


+ 10 - 0
src/constants.js

@@ -16,6 +16,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 export const coriolisUrl = (window.env && window.env.CORIOLIS_URL) || '/'
 
+let licenceUrl = (window.env && window.env.CORIOLIS_LICENSING_BASE_URL) || ''
+
+if (!licenceUrl) {
+  let coriolisBaseUrlExp = /http(?:s?):\/\/(.*?)(?:\/|$)/.exec(coriolisUrl)
+  if (coriolisBaseUrlExp && coriolisBaseUrlExp.length) {
+    licenceUrl = `http://${coriolisBaseUrlExp[1]}:37667/licensing/v1`
+  }
+}
+
 export const servicesUrl = {
   identity: `${coriolisUrl}identity/auth/tokens`,
   projects: `${coriolisUrl}identity/auth/projects`,
@@ -25,6 +34,7 @@ export const servicesUrl = {
   migrations: `${coriolisUrl}coriolis/migrations`,
   barbican: `${coriolisUrl}barbican`,
   openId: `${coriolisUrl}identity/OS-FEDERATION/identity_providers/google/protocols/openid/auth`,
+  licence: licenceUrl,
 }
 
 export const navigationMenu = [

+ 1 - 0
src/index.js

@@ -14,6 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
+import 'babel-polyfill'
 import 'react-hot-loader/patch'
 import React from 'react'
 import { render } from 'react-dom'

+ 48 - 0
src/sources/LincenceSource.js

@@ -0,0 +1,48 @@
+/*
+Copyright (C) 2019  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/>.
+*/
+
+// @flow
+import Api from '../utils/ApiCaller'
+
+import { servicesUrl } from '../constants'
+
+import type { Licence } from '../types/Licence'
+
+class LicenceSource {
+  async loadLicenceInfo(): Promise<Licence> {
+    let url = `${servicesUrl.licence}/licence-status`
+    let response = await Api.send({ url })
+    let root = response.data.licence_status
+    return ({
+      currentPeriodStart: new Date(root.current_period_start),
+      currentPeriodEnd: new Date(root.current_period_end),
+      performedMigrations: root.performed_migrations,
+      performedReplicas: root.performed_replicas,
+      totalMigations: root.total_migrations,
+      totalReplicas: root.total_replicas,
+    })
+  }
+
+  async addLicence(licence: string) {
+    let url = `${servicesUrl.licence}/licences`
+    await Api.send({
+      url,
+      method: 'POST',
+      headers: { 'Content-Type': 'application/x-pem-file' },
+      data: licence,
+    })
+  }
+}
+
+export default new LicenceSource()

+ 59 - 0
src/stores/LicenceStore.js

@@ -0,0 +1,59 @@
+/*
+Copyright (C) 2019  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/>.
+*/
+
+// @flow
+
+import { observable, action, runInAction } from 'mobx'
+
+import licenceSource from '../sources/LincenceSource'
+import type { Licence } from '../types/Licence'
+
+class LicenceStore {
+  @observable loadingLicenceInfo: boolean = false
+  @observable licenceInfo: ?Licence = null
+  @observable addingLicence: boolean = false
+
+  @action async loadLicenceInfo() {
+    this.loadingLicenceInfo = true
+    try {
+      let licence = await licenceSource.loadLicenceInfo()
+      runInAction(() => {
+        this.licenceInfo = licence
+        this.loadingLicenceInfo = false
+      })
+    } catch (ex) {
+      runInAction(() => {
+        this.loadingLicenceInfo = false
+      })
+      throw ex
+    }
+  }
+
+  @action async addLicence(licence: string) {
+    this.addingLicence = true
+    try {
+      await licenceSource.addLicence(licence)
+      runInAction(() => {
+        this.addingLicence = false
+      })
+    } catch (ex) {
+      runInAction(() => {
+        this.addingLicence = false
+      })
+      throw ex
+    }
+  }
+}
+
+export default new LicenceStore()

+ 24 - 0
src/types/Licence.js

@@ -0,0 +1,24 @@
+/*
+Copyright (C) 2019  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/>.
+*/
+
+// @flow
+
+export type Licence = {
+  currentPeriodStart: Date,
+  currentPeriodEnd: Date,
+  performedMigrations: number,
+  performedReplicas: number,
+  totalMigations: number,
+  totalReplicas: number,
+}

+ 4 - 4
src/utils/ApiCaller.js

@@ -118,10 +118,10 @@ class ApiCaller {
           // that falls out of the range of 2xx
           if (
             (error.response.status !== 401 || !isOnLoginPage()) &&
-            !options.quietError &&
-            error.response.data) {
+            !options.quietError) {
             let data = error.response.data
-            let message = (data.error && data.error.message) || data.description
+            let message = (data && data.error && data.error.message) || (data && data.description)
+            message = message || `${error.response.statusText || error.response.status} ${options.url}`
             if (message) {
               notificationStore.alert(message, 'error')
             }
@@ -143,7 +143,7 @@ class ApiCaller {
           // The request was made but no response was received
           // `error.request` is an instance of XMLHttpRequest
           if (!isOnLoginPage()) {
-            notificationStore.alert('Request failed, there might be a problem with the connection to the server.', 'error')
+            notificationStore.alert(`Request failed, there might be a problem with the connection to the server. ${options.url}`, 'error')
           }
           logger.log({
             url: axiosOptions.url,

+ 1 - 0
yarn.lock

@@ -1435,6 +1435,7 @@ babel-plugin-transform-undefined-to-void@^6.8.3:
 babel-polyfill@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
+  integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=
   dependencies:
     babel-runtime "^6.26.0"
     core-js "^2.5.0"