Bläddra i källkod

Merge pull request #611 from smiclea/dashboard-licence

Add licence from dashboard page, if expired
Nashwan Azhari 5 år sedan
förälder
incheckning
2ae8963a48

+ 2 - 0
src/components/atoms/CopyValue/CopyValue.tsx

@@ -47,6 +47,7 @@ type Props = {
   capitalize?: boolean,
   'data-test-id'?: string,
   onCopy?: (value: string) => void,
+  style?: React.CSSProperties
 }
 @observer
 class CopyValue extends React.Component<Props> {
@@ -71,6 +72,7 @@ class CopyValue extends React.Component<Props> {
         onMouseUp={(e: { stopPropagation: () => void }) => { e.stopPropagation() }}
         data-test-id={this.props['data-test-id'] || 'copyValue'}
         capitalize={this.props.capitalize}
+        style={this.props.style}
       >
         <Value
           data-test-id="copyValue-value"

+ 5 - 1
src/components/organisms/DashboardContent/DashboardContent.tsx

@@ -28,7 +28,7 @@ import Palette from '../../styleUtils/Palette'
 import type { Endpoint } from '../../../@types/Endpoint'
 import type { Project } from '../../../@types/Project'
 import type { User } from '../../../@types/User'
-import type { Licence } from '../../../@types/Licence'
+import type { Licence, LicenceServerStatus } from '../../../@types/Licence'
 import type { NotificationItemData } from '../../../@types/NotificationItem'
 import { ReplicaItem, MigrationItem } from '../../../@types/MainItem'
 
@@ -65,11 +65,13 @@ type Props = {
   notificationItemsLoading: boolean,
   users: User[],
   licence: Licence | null,
+  licenceServerStatus: LicenceServerStatus | null
   licenceError: string | null,
   notificationItems: NotificationItemData[],
   isAdmin: boolean,
   onNewReplicaClick: () => void,
   onNewEndpointClick: () => void,
+  onAddLicenceClick: () => void,
 }
 type State = {
   useMobileLayout: boolean,
@@ -132,7 +134,9 @@ class DashboardContent extends React.Component<Props, State> {
       <LicenceModule
         licence={this.props.licence}
         loading={this.props.licenceLoading}
+        licenceServerStatus={this.props.licenceServerStatus}
         licenceError={this.props.licenceError}
+        onAddClick={this.props.onAddLicenceClick}
         style={{
           minWidth: MIDDLE_WIDTHS[2],
           width: MIDDLE_WIDTHS[2],

+ 64 - 10
src/components/organisms/DashboardContent/modules/LicenceModule/LicenceModule.tsx

@@ -23,7 +23,11 @@ import InfoIcon from '../../../../atoms/InfoIcon'
 import Palette from '../../../../styleUtils/Palette'
 import StyleProps from '../../../../styleUtils/StyleProps'
 
-import type { Licence } from '../../../../../@types/Licence'
+import type { Licence, LicenceServerStatus } from '../../../../../@types/Licence'
+import CopyValue from '../../../../atoms/CopyValue/CopyValue'
+import Button from '../../../../atoms/Button/Button'
+
+import licenceImage from '../../../Licence/images/licence'
 
 const Wrapper = styled.div<any>`
   flex-grow: 1;
@@ -44,7 +48,22 @@ const Module = styled.div<any>`
 const LicenceInfo = styled.div<any>`
   width: 100%;
 `
-const NoLicence = styled.div<any>``
+const LicenceError = styled.span`
+  p {
+    margin: 16px 0 0 0;
+    &:first-child {
+      margin: 0;
+    }
+  }
+`
+const ApplianceId = styled.div`
+  display: flex;
+  margin-top: 16px;
+`
+const AddLicenceButtonWrapper = styled.div`
+  margin-top: 32px;
+  text-align: center;
+`
 const TopInfo = styled.div<any>`
   display: flex;
 `
@@ -111,6 +130,13 @@ const ChartBody = styled.div<any>`
   background: ${props => props.color};
   height: 100%;
 `
+const Logo = styled.div`
+  width: 96px;
+  height: 96px;
+  margin: 0 auto;
+  transform: scale(0.7);
+  text-align: center;
+`
 const LoadingWrapper = styled.div<any>`
   overflow: hidden;
   display: flex;
@@ -122,9 +148,11 @@ const LoadingWrapper = styled.div<any>`
 
 type Props = {
   licence: Licence | null,
+  licenceServerStatus: LicenceServerStatus | null
   loading: boolean,
   style: any,
   licenceError: string | null,
+  onAddClick: () => void,
 }
 @observer
 class LicenceModule extends React.Component<Props> {
@@ -149,7 +177,8 @@ class LicenceModule extends React.Component<Props> {
           current: info.currentPerformedReplicas,
           total: info.currentAvailableReplicas,
           label: 'Current Replicas',
-          info: 'The number of replicas consumed over the number of replicas available in all currently active licences (including non-activated floating licences)',
+          info: `The number of replicas consumed over the number of replicas available in
+          all currently active licences (including non-activated floating licences)`,
         },
         {
           color: Palette.alert,
@@ -165,7 +194,8 @@ class LicenceModule extends React.Component<Props> {
           current: info.currentPerformedMigrations,
           total: info.currentAvailableMigrations,
           label: 'Current Migrations',
-          info: 'The number of migrations consumed over the number of migrations available in all currently active licences (including non-activated floating licences)',
+          info: `The number of migrations consumed over the number of migrations available in
+          all currently active licences (including non-activated floating licences)`,
         },
         {
           color: Palette.primary,
@@ -213,10 +243,34 @@ class LicenceModule extends React.Component<Props> {
     )
   }
 
-  renderNoLicence() {
-    const message = this.props.licenceError || 'Please contact Cloudbase Solutions with your Appliance ID in order to obtain a Coriolis® licence.'
+  renderLicenceError() {
+    return (
+      <LicenceError>{this.props.licenceError?.split('\n').map(str => <p>{str}</p>)}</LicenceError>
+    )
+  }
+
+  renderLicenceExpired(licence: Licence, serverStatus: LicenceServerStatus) {
     return (
-      <NoLicence>{message}</NoLicence>
+      <LicenceError>
+        <p>
+          Please contact Cloudbase Solutions with your Appliance ID
+          in order to obtain a Coriolis® licence.
+        </p>
+        <ApplianceId>
+          Appliance ID: <CopyValue
+            style={{ marginLeft: '8px' }}
+            value={`${licence.applianceId}-licence${serverStatus.supported_licence_versions[0]}`}
+          />
+        </ApplianceId>
+        <AddLicenceButtonWrapper>
+          <Logo
+            dangerouslySetInnerHTML={
+              { __html: licenceImage(Palette.grayscale[5]) }
+            }
+          />
+          <Button primary onClick={this.props.onAddClick}>Add Licence</Button>
+        </AddLicenceButtonWrapper>
+      </LicenceError>
     )
   }
 
@@ -231,16 +285,16 @@ class LicenceModule extends React.Component<Props> {
   render() {
     const licence = this.props.licence
     let moduleContent = null
-    if (licence) {
+    if (licence && this.props.licenceServerStatus) {
       if (new Date(licence.earliestLicenceExpiryDate).getTime() > new Date().getTime()) {
         moduleContent = this.renderLicenceStatusText(licence)
       } else {
-        moduleContent = this.renderNoLicence()
+        moduleContent = this.renderLicenceExpired(licence, this.props.licenceServerStatus)
       }
     } else if (this.props.loading) {
       moduleContent = this.renderLoading()
     } else if (this.props.licenceError) {
-      moduleContent = this.renderNoLicence()
+      moduleContent = this.renderLicenceError()
     }
 
     return licence || this.props.loading || this.props.licenceError ? (

+ 2 - 1
src/components/organisms/Licence/Licence.tsx

@@ -111,6 +111,7 @@ type Props = {
   addMode: boolean,
   onAddLicence: (licence: string) => void,
   addingLicence: boolean,
+  backButtonText: string
 }
 type State = {
   licence: string,
@@ -296,7 +297,7 @@ class LicenceC extends React.Component<Props, State> {
             secondary
             large
             onClick={() => { this.props.onAddModeChange(false) }}
-          >Back
+          >{this.props.backButtonText}
           </Button>
         )
           : (

+ 5 - 5
src/components/organisms/Licence/images/licence.ts

@@ -5,7 +5,7 @@ export default (color: string) => `
 
     <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="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>
@@ -14,10 +14,10 @@ export default (color: string) => `
                     <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 id="Group" stroke-width="1" fill-rule="evenodd" transform="translate(44.000000, 63.000000)" fill="none">
+                        <path fill="none" 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 fill="none" 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 fill="none" 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>

+ 22 - 11
src/components/organisms/PageHeader/PageHeader.tsx

@@ -85,7 +85,8 @@ type State = {
   selectedMinionPoolEndpoint: EndpointType | null
   showUserModal: boolean,
   showProjectModal: boolean,
-  showAbout: boolean,
+  showAddLicenceModal: boolean,
+  showAboutModal: boolean,
   providerType: ProviderTypes | null,
   uploadedEndpoint: EndpointType | null,
   multiValidating: boolean,
@@ -103,7 +104,8 @@ class PageHeader extends React.Component<Props, State> {
     showProjectModal: false,
     providerType: null,
     uploadedEndpoint: null,
-    showAbout: false,
+    showAboutModal: false,
+    showAddLicenceModal: false,
     multiValidating: false,
     selectedMinionPoolPlatform: 'source',
   }
@@ -138,7 +140,7 @@ class PageHeader extends React.Component<Props, State> {
   handleUserItemClick(item: { value: string }) {
     switch (item.value) {
       case 'about':
-        this.setState({ showAbout: true })
+        this.setState({ showAboutModal: true })
         if (this.props.onModalOpen) {
           this.props.onModalOpen()
         }
@@ -180,6 +182,12 @@ class PageHeader extends React.Component<Props, State> {
         }
         this.setState({ showProjectModal: true })
         break
+      case 'licence':
+        if (this.props.onModalOpen) {
+          this.props.onModalOpen()
+        }
+        this.setState({ showAddLicenceModal: true })
+        break
       default:
     }
   }
@@ -321,7 +329,8 @@ class PageHeader extends React.Component<Props, State> {
       || this.state.showMinionPoolModal
       || this.state.showProjectModal
       || this.state.showUserModal
-      || this.state.showAbout
+      || this.state.showAboutModal
+      || this.state.showAddLicenceModal
     ) {
       return
     }
@@ -430,13 +439,15 @@ class PageHeader extends React.Component<Props, State> {
             onUpdateClick={project => { this.handleProjectModalUpdateClick(project) }}
           />
         ) : null}
-        {this.state.showAbout ? (
-          <AboutModal onRequestClose={() => {
-            this.setState({ showAbout: false })
-            if (this.props.onModalClose) {
-              this.props.onModalClose()
-            }
-          }}
+        {this.state.showAboutModal || this.state.showAddLicenceModal ? (
+          <AboutModal
+            licenceAddMode={this.state.showAddLicenceModal}
+            onRequestClose={() => {
+              this.setState({ showAboutModal: false, showAddLicenceModal: false })
+              if (this.props.onModalClose) {
+                this.props.onModalClose()
+              }
+            }}
           />
         ) : null}
       </Wrapper>

+ 21 - 8
src/components/pages/AboutModal/AboutModal.tsx

@@ -80,19 +80,21 @@ const TextLine = styled.div<any>`
 
 type Props = {
   onRequestClose: () => void,
+  licenceAddMode?: boolean
 }
 
 type State = {
   licenceAddMode: boolean,
 }
-
 @observer
 class AboutModal extends React.Component<Props, State> {
   state = {
     licenceAddMode: false,
   }
 
-  UNSAFE_componentWillMount() {
+  constructor(props: Props) {
+    super(props)
+
     if (userStore.loggedUser?.isAdmin) {
       licenceStore.loadLicenceInfo({ showLoading: true })
     }
@@ -104,20 +106,24 @@ class AboutModal extends React.Component<Props, State> {
     }
     await licenceStore.addLicence(licence, licenceStore.licenceInfo.applianceId)
     licenceStore.loadLicenceInfo()
-    this.setState({ licenceAddMode: false })
+    if (this.props.licenceAddMode) {
+      this.props.onRequestClose()
+    } else {
+      this.setState({ licenceAddMode: false })
+    }
   }
 
   render() {
     return (
       <Modal
-        title="About"
+        title={this.state.licenceAddMode || this.props.licenceAddMode ? 'Add Licence' : 'About'}
         isOpen
         onRequestClose={() => { this.props.onRequestClose() }}
       >
         <Wrapper>
-          {!this.state.licenceAddMode ? <Gradient /> : null}
+          {!this.state.licenceAddMode && !this.props.licenceAddMode ? <Gradient /> : null}
           <Content>
-            {!this.state.licenceAddMode ? (
+            {!this.state.licenceAddMode && !this.props.licenceAddMode ? (
               <AboutContentWrapper>
                 <Logo />
                 <Text>
@@ -133,8 +139,15 @@ class AboutModal extends React.Component<Props, State> {
               licenceError={licenceStore.licenceInfoError}
               loadingLicenceInfo={licenceStore.loadingLicenceInfo}
               onRequestClose={this.props.onRequestClose}
-              addMode={this.state.licenceAddMode}
-              onAddModeChange={licenceAddMode => { this.setState({ licenceAddMode }) }}
+              addMode={this.state.licenceAddMode || this.props.licenceAddMode || false}
+              backButtonText={this.props.licenceAddMode ? 'Cancel' : 'Back'}
+              onAddModeChange={licenceAddMode => {
+                if (this.props.licenceAddMode) {
+                  this.props.onRequestClose()
+                } else {
+                  this.setState({ licenceAddMode })
+                }
+              }}
               onAddLicence={licence => { this.handleAddLicence(licence) }}
               addingLicence={licenceStore.addingLicence}
             />

+ 7 - 1
src/components/pages/DashboardPage/DashboardPage.tsx

@@ -82,6 +82,10 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
     this.pageHeaderRef.handleNewItem('endpoint')
   }
 
+  handleAddLicenceClick() {
+    this.pageHeaderRef.handleNewItem('licence')
+  }
+
   async pollData(showLoading: boolean) {
     if (this.state.modalIsOpen || this.stopPolling) {
       return
@@ -107,7 +111,7 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
   async loadAdminData(showLoading: boolean) {
     await Utils.waitFor(() => Boolean(userStore.loggedUser && userStore.loggedUser.isAdmin),
       30000, 100)
-    if (userStore.loggedUser && userStore.loggedUser.isAdmin) {
+    if (userStore.loggedUser?.isAdmin) {
       userStore.getAllUsers({ skipLog: true, showLoading })
       licenceStore.loadLicenceInfo({ skipLog: true, showLoading })
     }
@@ -127,6 +131,7 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
               users={userStore.users}
               projects={projectStore.projects}
               licence={licenceStore.licenceInfo}
+              licenceServerStatus={licenceStore.licenceServerStatus}
               isAdmin={Boolean(userStore.loggedUser && userStore.loggedUser.isAdmin)}
               notificationItems={notificationStore.notificationItems}
               notificationItemsLoading={notificationStore.loading}
@@ -139,6 +144,7 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
               replicasLoading={replicaStore.loading}
               onNewReplicaClick={() => { this.props.history.push('/wizard/replica') }}
               onNewEndpointClick={() => { this.handleNewEndpointClick() }}
+              onAddLicenceClick={() => { this.handleAddLicenceClick() }}
             />
           )}
           headerComponent={(

+ 9 - 2
src/stores/LicenceStore.ts

@@ -30,7 +30,9 @@ class LicenceStore {
   @observable licenceInfoError: string | null = null
 
   @action async loadLicenceInfo(opts?: { skipLog?: boolean, showLoading?: boolean }) {
-    if (opts && opts.showLoading) this.loadingLicenceInfo = true
+    if (opts?.showLoading && !this.licenceInfo && !this.licenceInfoError) {
+      this.loadingLicenceInfo = true
+    }
     try {
       const ids = await licenceSource.loadAppliancesIds(opts?.skipLog)
       if (!ids.length || ids.length > 1) {
@@ -40,18 +42,23 @@ class LicenceStore {
           }
           this.loadingLicenceInfo = false
         })
+        return
       }
-      this.licenceInfoError = null
       const applianceId = ids[0]
       const [licenceServerStatus, licenceInfo] = await Promise.all([
         licenceSource.loadLicenceServerStatus(opts?.skipLog),
         licenceSource.loadLicenceInfo(applianceId, opts?.skipLog),
       ])
       runInAction(() => {
+        this.licenceInfoError = null
         this.licenceInfo = licenceInfo
         this.licenceServerStatus = licenceServerStatus
         this.loadingLicenceInfo = false
       })
+    } catch (err) {
+      runInAction(() => {
+        this.licenceInfoError = `There was an error contacting Coriolis® Licensing Server. Request status: ${err.status}\nPlease contact Coriolis® Support.`
+      })
     } finally {
       runInAction(() => {
         this.loadingLicenceInfo = false