Просмотр исходного кода

Merge pull request #518 from smiclea/licence

Add support for new licensing API structure
Nashwan Azhari 5 лет назад
Родитель
Сommit
0d48278e2e

+ 10 - 6
src/@types/Licence.ts

@@ -13,11 +13,15 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
 export type Licence = {
-  currentPeriodStart: Date,
-  currentPeriodEnd: Date,
-  performedMigrations: number,
-  performedReplicas: number,
-  totalMigations: number,
-  totalReplicas: number,
   applianceId: string,
+  earliestLicenceExpiryDate: Date,
+  latestLicenceExpiryDate: Date,
+  currentPerformedMigrations: number,
+  currentPerformedReplicas: number,
+  lifetimePerformedMigrations: number,
+  lifetimePerformedReplicas: number,
+  currentAvailableMigrations: number,
+  currentAvailableReplicas: number,
+  lifetimeAvailableMigrations: number,
+  lifetimeAvailableReplicas: number,
 }

+ 9 - 7
src/components/organisms/DashboardContent/DashboardContent.tsx

@@ -32,7 +32,7 @@ import type { Licence } from '../../../@types/Licence'
 import type { NotificationItemData } from '../../../@types/NotificationItem'
 import { ReplicaItem, MigrationItem } from '../../../@types/MainItem'
 
-const MIDDLE_WIDTHS = ['264px', '264px', '264px']
+const MIDDLE_WIDTHS = ['264px', '264px', '450px']
 
 const Wrapper = styled.div<any>`
   margin-bottom: 64px;
@@ -65,6 +65,7 @@ type Props = {
   notificationItemsLoading: boolean,
   users: User[],
   licence: Licence | null,
+  licenceError: string | null,
   notificationItems: NotificationItemData[],
   isAdmin: boolean,
   onNewReplicaClick: () => void,
@@ -111,8 +112,8 @@ class DashboardContent extends React.Component<Props, State> {
         notificationItems={this.props.notificationItems}
         loading={this.props.notificationItemsLoading}
         style={this.state.useMobileLayout ? null : {
-          minWidth: MIDDLE_WIDTHS[1],
-          width: MIDDLE_WIDTHS[1],
+          minWidth: MIDDLE_WIDTHS[0],
+          width: MIDDLE_WIDTHS[0],
         }}
         onNewClick={this.props.onNewReplicaClick}
       />,
@@ -123,17 +124,18 @@ class DashboardContent extends React.Component<Props, State> {
         loading={this.props.replicasLoading
           || this.props.migrationsLoading || this.props.endpointsLoading}
         style={{
-          minWidth: MIDDLE_WIDTHS[2],
-          width: MIDDLE_WIDTHS[2],
+          minWidth: MIDDLE_WIDTHS[1],
+          width: MIDDLE_WIDTHS[1],
         }}
         onNewClick={this.props.onNewEndpointClick}
       />,
       <LicenceModule
         licence={this.props.licence}
         loading={this.props.licenceLoading}
+        licenceError={this.props.licenceError}
         style={{
-          minWidth: MIDDLE_WIDTHS[0],
-          width: MIDDLE_WIDTHS[0],
+          minWidth: MIDDLE_WIDTHS[2],
+          width: MIDDLE_WIDTHS[2],
         }}
       />,
     ]

+ 97 - 45
src/components/organisms/DashboardContent/modules/LicenceModule/LicenceModule.tsx

@@ -18,6 +18,7 @@ import styled from 'styled-components'
 import moment from 'moment'
 
 import StatusImage from '../../../../atoms/StatusImage'
+import InfoIcon from '../../../../atoms/InfoIcon'
 
 import Palette from '../../../../styleUtils/Palette'
 import StyleProps from '../../../../styleUtils/StyleProps'
@@ -40,14 +41,15 @@ const Module = styled.div<any>`
   padding: 24px 16px 16px 16px;
   height: 232px;
 `
-const LicenceInfo = styled.div<any>``
+const LicenceInfo = styled.div<any>`
+  width: 100%;
+`
 const NoLicence = styled.div<any>``
 const TopInfo = styled.div<any>`
   display: flex;
 `
 const TopInfoText = styled.div<any>`
   flex-grow: 1;
-  margin-top: 8px;
 `
 const TopInfoDate = styled.div<any>`
   ${StyleProps.exactWidth('76px')}
@@ -78,13 +80,16 @@ const TopInfoDateBottom = styled.div<any>`
   font-weight: ${StyleProps.fontWeights.extraLight};
 `
 const Charts = styled.div<any>`
+  margin-top: -8px;
+`
+const ChartRow = styled.div`
+  display: flex;
+  margin-left: -32px;
   margin-top: 32px;
 `
 const Chart = styled.div<any>`
-  margin-top: 32px;
-  &:first-child {
-    margin-top: 0;
-  }
+  width: 100%;
+  margin-left: 32px;
 `
 const ChartHeader = styled.div<any>`
   display: flex;
@@ -119,47 +124,89 @@ type Props = {
   licence: Licence | null,
   loading: boolean,
   style: any,
+  licenceError: string | null,
 }
 @observer
 class LicenceModule extends React.Component<Props> {
+  renderExpiration(date: Date) {
+    const dateMoment = moment(date)
+    const days = dateMoment.diff(new Date(), 'days')
+    if (days === 0) {
+      return (
+        <span>today at <b>{dateMoment.utc().format('HH:mm')} UTC</b></span>
+      )
+    }
+    return (
+      <span>on <b>{dateMoment.format('DD MMM YYYY')}</b></span>
+    )
+  }
+
   renderLicenceStatusText(info: Licence): React.ReactNode {
-    const currentPeriod = moment(info.currentPeriodEnd)
-    const days = currentPeriod.diff(new Date(), 'days')
-    const graphData = [{
-      color: Palette.alert,
-      current: info.performedReplicas,
-      total: info.totalReplicas,
-      label: 'Replicas',
-    }, {
-      color: Palette.primary,
-      current: info.performedMigrations,
-      total: info.totalMigations,
-      label: 'Migrations',
-    }]
+    const graphDataRows = [
+      [
+        {
+          color: Palette.alert,
+          current: info.currentPerformedReplicas,
+          total: info.currentAvailableReplicas,
+          label: 'Current Replicas',
+          info: 'The number of replicas performed with current licence over the number of replicas available in current licence',
+        },
+        {
+          color: Palette.alert,
+          current: info.lifetimePerformedReplicas,
+          total: info.lifetimeAvailableReplicas,
+          label: 'Lifetime Replicas',
+          info: 'The number of lifetime performed replicas over the number of lifetime available replicas',
+        },
+      ],
+      [
+        {
+          color: Palette.primary,
+          current: info.currentPerformedMigrations,
+          total: info.currentAvailableMigrations,
+          label: 'Current Migrations',
+          info: 'The number of migrations performed with current licence over the number of migrations available in current licence',
+        },
+        {
+          color: Palette.primary,
+          current: info.lifetimePerformedMigrations,
+          total: info.lifetimeAvailableMigrations,
+          label: 'Lifetime Migrations',
+          info: 'The number of lifetime performed migrations over the number of lifetime available migrations',
+        },
+      ],
+    ]
+    const latestLicenceExpiryDate = moment(info.latestLicenceExpiryDate)
     return (
       <LicenceInfo>
         <TopInfo>
           <TopInfoText>
-            Coriolis® Licence is active until&nbsp;
-            {currentPeriod.format('DD MMM YYYY')}
-            &nbsp;({days} days from now).
+            Earliest Coriolis® Licence expires&nbsp;
+            {this.renderExpiration(info.earliestLicenceExpiryDate)}.<br /><br />
+            Latest Coriolis® Licence expires {this.renderExpiration(info.latestLicenceExpiryDate)}.
           </TopInfoText>
           <TopInfoDate>
-            <TopInfoDateTop>{currentPeriod.format('MMM')} &#39;{currentPeriod.format('YY')}</TopInfoDateTop>
-            <TopInfoDateBottom>{currentPeriod.format('DD')}</TopInfoDateBottom>
+            <TopInfoDateTop>{latestLicenceExpiryDate.format('MMM')} &#39;{latestLicenceExpiryDate.format('YY')}</TopInfoDateTop>
+            <TopInfoDateBottom>{latestLicenceExpiryDate.format('DD')}</TopInfoDateBottom>
           </TopInfoDate>
         </TopInfo>
         <Charts>
-          {graphData.map(data => (
-            <Chart key={data.label}>
-              <ChartHeader>
-                <ChartHeaderCurrent>{data.current} {data.label}</ChartHeaderCurrent>
-                <ChartHeaderTotal>{data.total}</ChartHeaderTotal>
-              </ChartHeader>
-              <ChartBodyWrapper>
-                <ChartBody color={data.color} width={(data.current / data.total) * 100} />
-              </ChartBodyWrapper>
-            </Chart>
+          {graphDataRows.map(row => (
+            <ChartRow>
+              {row.map(data => (
+                <Chart key={data.label}>
+                  <ChartHeader>
+                    <ChartHeaderCurrent>
+                      {data.current} {data.label} <InfoIcon marginBottom={-3} text={data.info} />
+                    </ChartHeaderCurrent>
+                    <ChartHeaderTotal>{data.total}</ChartHeaderTotal>
+                  </ChartHeader>
+                  <ChartBodyWrapper>
+                    <ChartBody color={data.color} width={(data.current / data.total) * 100} />
+                  </ChartBodyWrapper>
+                </Chart>
+              ))}
+            </ChartRow>
           ))}
         </Charts>
       </LicenceInfo>
@@ -167,11 +214,9 @@ 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.'
     return (
-      <NoLicence>
-        Please contact Cloudbase Solutions with your
-        Appliance ID in order to obtain a Coriolis® licence.
-      </NoLicence>
+      <NoLicence>{message}</NoLicence>
     )
   }
 
@@ -185,17 +230,24 @@ class LicenceModule extends React.Component<Props> {
 
   render() {
     const licence = this.props.licence
-    let days: number | null = null
+    let moduleContent = null
     if (licence) {
-      const currentPeriod = moment(licence.currentPeriodEnd)
-      days = currentPeriod.diff(new Date(), 'days')
+      if (new Date(licence.earliestLicenceExpiryDate).getTime() > new Date().getTime()) {
+        moduleContent = this.renderLicenceStatusText(licence)
+      } else {
+        moduleContent = this.renderNoLicence()
+      }
+    } else if (this.props.loading) {
+      moduleContent = this.renderLoading()
+    } else if (this.props.licenceError) {
+      moduleContent = this.renderNoLicence()
     }
-    return licence || this.props.loading ? (
+
+    return licence || this.props.loading || this.props.licenceError ? (
       <Wrapper style={this.props.style}>
-        <Title>Licence</Title>
+        <Title>Current Licence</Title>
         <Module>
-          {licence ? days && days > 0 ? this.renderLicenceStatusText(licence)
-            : this.renderNoLicence() : this.props.loading ? this.renderLoading() : null}
+          {moduleContent}
         </Module>
       </Wrapper>
     ) : null

+ 28 - 26
src/components/organisms/Licence/Licence.tsx

@@ -77,10 +77,6 @@ const LoadingWrapper = styled.div<any>`
   flex-direction: column;
   align-items: center;
 `
-const LoadingText = styled.div<any>`
-  font-size: 18px;
-  margin-top: 32px;
-`
 const ButtonsWrapper = styled.div<any>`
   display: flex;
   margin-top: 48px;
@@ -107,6 +103,7 @@ const FakeFileInput = styled.input`
 
 type Props = {
   licenceInfo: Licence | null,
+  licenceError: string | null,
   loadingLicenceInfo: boolean,
   onRequestClose: () => void,
   onAddModeChange: (addMode: boolean) => void,
@@ -228,26 +225,40 @@ class LicenceC extends React.Component<Props, State> {
     return (
       <LoadingWrapper>
         <StatusImage loading />
-        <LoadingText>Loading licence info ...</LoadingText>
       </LoadingWrapper>
     )
   }
 
+  renderExpiration(date: Date) {
+    const dateMoment = moment(date)
+    const days = dateMoment.diff(new Date(), 'days')
+    if (days === 0) {
+      return (
+        <span>today at <b>{dateMoment.utc().format('HH:mm')} UTC</b></span>
+      )
+    }
+    return (
+      <span>on <b>{dateMoment.format('DD MMM YYYY')} ({days} days from now)</b></span>
+    )
+  }
+
   renderLicenceStatusText(info: Licence): React.ReactNode {
-    const currentPeriod = moment(info.currentPeriodEnd)
-    const days = currentPeriod.diff(new Date(), 'days')
-    if (days < 0) {
+    if (new Date(info.earliestLicenceExpiryDate).getTime() < new Date().getTime()) {
       return 'Please contact Cloudbase Solutions with your Appliance ID in order to obtain a Coriolis® licence'
     }
     return (
       <LicenceRowDescription>
-        Coriolis® Licence is active until&nbsp;
-        {currentPeriod.format('DD MMM YYYY')}
-        &nbsp;({days} days from now).
+        Earliest Coriolis® Licence expires&nbsp;
+        {this.renderExpiration(info.earliestLicenceExpiryDate)}.<br />
+        Latest Coriolis® Licence expires {this.renderExpiration(info.latestLicenceExpiryDate)}.
       </LicenceRowDescription>
     )
   }
 
+  renderLicenceError(error: string) {
+    return <LicenceInfoWrapper>{error}</LicenceInfoWrapper>
+  }
+
   renderLicenceInfo(info: Licence) {
     return (
       <LicenceInfoWrapper>
@@ -264,20 +275,6 @@ class LicenceC extends React.Component<Props, State> {
             {this.renderLicenceStatusText(info)}
           </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>
         <LicenceRow>
           <LicenceRowContent>
             <LicenceRowLabel>Appliance ID</LicenceRowLabel>
@@ -371,10 +368,15 @@ class LicenceC extends React.Component<Props, State> {
   }
 
   render() {
-    const showInfo = !this.props.loadingLicenceInfo && !this.props.addMode
+    const showInfo = !this.props.loadingLicenceInfo
+      && !this.props.addMode && !this.props.licenceError
+    const showError = !this.props.loadingLicenceInfo && !this.props.addMode
+
     return (
       <Wrapper>
         {showInfo && this.props.licenceInfo ? this.renderLicenceInfo(this.props.licenceInfo) : null}
+        {showError && this.props.licenceError
+          ? this.renderLicenceError(this.props.licenceError) : null}
         {this.props.addMode ? this.renderLicenceAdd() : null}
         {this.props.loadingLicenceInfo ? this.renderLicenceInfoLoading() : null}
         {this.renderButtons()}

+ 11 - 4
src/components/pages/AboutModal/AboutModal.tsx

@@ -23,6 +23,7 @@ import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 
 import licenceStore from '../../../stores/LicenceStore'
+import userStore from '../../../stores/UserStore'
 
 import logoImage from './images/coriolis-logo.svg'
 
@@ -59,8 +60,8 @@ const Logo = styled.div<any>`
   ${StyleProps.exactHeight('71px')}
   background: url('${logoImage}') center no-repeat;
 `
-const Text = styled.div<any>`
-  margin: 48px 0;
+const Text = styled.div`
+  margin: 48px 0 32px 0;
   color: ${Palette.grayscale[5]};
   font-size: 12px;
 `
@@ -93,11 +94,16 @@ class AboutModal extends React.Component<Props, State> {
 
   UNSAFE_componentWillMount() {
     licenceStore.loadVersion()
-    licenceStore.loadLicenceInfo()
+    if (userStore.loggedUser && userStore.loggedUser.isAdmin) {
+      licenceStore.loadLicenceInfo({ showLoading: true })
+    }
   }
 
   async handleAddLicence(licence: string) {
-    await licenceStore.addLicence(licence)
+    if (!licenceStore.licenceInfo) {
+      throw new Error('Licence info not loaded')
+    }
+    await licenceStore.addLicence(licence, licenceStore.licenceInfo.applianceId)
     licenceStore.loadLicenceInfo()
     this.setState({ licenceAddMode: false })
   }
@@ -124,6 +130,7 @@ class AboutModal extends React.Component<Props, State> {
             ) : null}
             <LicenceComponent
               licenceInfo={licenceStore.licenceInfo}
+              licenceError={licenceStore.licenceInfoError}
               loadingLicenceInfo={licenceStore.loadingLicenceInfo}
               onRequestClose={this.props.onRequestClose}
               addMode={this.state.licenceAddMode}

+ 3 - 2
src/components/pages/DashboardPage/DashboardPage.tsx

@@ -101,7 +101,6 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
       migrationStore.getMigrations({ skipLog: true, showLoading }),
       endpointStore.getEndpoints({ skipLog: true, showLoading }),
       projectStore.getProjects({ skipLog: true, showLoading }),
-      licenceStore.loadLicenceInfo({ skipLog: true, showLoading }),
     ])
   }
 
@@ -109,7 +108,8 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
     await Utils.waitFor(() => Boolean(userStore.loggedUser && userStore.loggedUser.isAdmin),
       30000, 100)
     if (userStore.loggedUser && userStore.loggedUser.isAdmin) {
-      await userStore.getAllUsers({ skipLog: true, showLoading })
+      userStore.getAllUsers({ skipLog: true, showLoading })
+      licenceStore.loadLicenceInfo({ skipLog: true, showLoading })
     }
   }
 
@@ -135,6 +135,7 @@ class ProjectsPage extends React.Component<{ history: any }, State> {
               projectsLoading={projectStore.projects.length === 0}
               usersLoading={userStore.users.length === 0}
               licenceLoading={licenceStore.loadingLicenceInfo}
+              licenceError={licenceStore.licenceInfoError}
               replicasLoading={replicaStore.loading}
               onNewReplicaClick={() => { this.props.history.push('/wizard/replica') }}
               onNewEndpointClick={() => { this.handleNewEndpointClick() }}

+ 25 - 13
src/sources/LincenceSource.ts

@@ -18,23 +18,35 @@ import configLoader from '../utils/Config'
 import type { Licence } from '../@types/Licence'
 
 class LicenceSource {
-  async loadLicenceInfo(skipLog?: boolean | null): Promise<Licence> {
-    const url = `${configLoader.config.servicesUrls.coriolisLicensing}/licence-status`
+  async loadAppliancesIds(skipLog?: boolean | null): Promise<string[]> {
+    const url = `${configLoader.config.servicesUrls.coriolisLicensing}/appliances`
     const response = await Api.send({ url, quietError: true, skipLog })
-    const 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,
+    return response.data.appliances.map((a: any) => a.id)
+  }
+
+  async loadLicenceInfo(applianceId: string, skipLog?: boolean | null): Promise<Licence> {
+    const url = `${configLoader.config.servicesUrls.coriolisLicensing}/appliances/${applianceId}/status`
+    const response = await Api.send({ url, quietError: true, skipLog })
+    const root = response.data.appliance_licence_status
+    const licence: Licence = {
       applianceId: root.appliance_id,
-    })
+      earliestLicenceExpiryDate: new Date(root.earliest_licence_expiry_time),
+      latestLicenceExpiryDate: new Date(root.latest_licence_expiry_time),
+      currentPerformedMigrations: root.current_performed_migrations,
+      currentPerformedReplicas: root.current_performed_replicas,
+      lifetimePerformedMigrations: root.lifetime_perfomed_migrations,
+      lifetimePerformedReplicas: root.lifetime_perfomed_replicas,
+      currentAvailableMigrations: root.current_available_migrations,
+      currentAvailableReplicas: root.current_available_replicas,
+      lifetimeAvailableMigrations: root.lifetime_available_migrations,
+      lifetimeAvailableReplicas: root.lifetime_available_replicas,
+    }
+
+    return licence
   }
 
-  async addLicence(licence: string) {
-    const url = `${configLoader.config.servicesUrls.coriolisLicensing}/licences`
+  async addLicence(licence: string, applianceId: string) {
+    const url = `${configLoader.config.servicesUrls.coriolisLicensing}/appliances/${applianceId}/licences`
     await Api.send({
       url,
       method: 'POST',

+ 18 - 6
src/stores/LicenceStore.ts

@@ -28,6 +28,8 @@ class LicenceStore {
 
   @observable version: string | null = null
 
+  @observable licenceInfoError: string | null = null
+
   async loadVersion(): Promise<string> {
     if (this.version) {
       return this.version
@@ -43,23 +45,33 @@ class LicenceStore {
   @action async loadLicenceInfo(opts?: { skipLog?: boolean, showLoading?: boolean }) {
     if (opts && opts.showLoading) this.loadingLicenceInfo = true
     try {
-      const licence = await licenceSource.loadLicenceInfo(opts && opts.skipLog)
+      const ids = await licenceSource.loadAppliancesIds(opts && opts.skipLog)
+      if (!ids.length || ids.length > 1) {
+        runInAction(() => {
+          if (ids.length > 1) {
+            this.licenceInfoError = '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.'
+          }
+          this.loadingLicenceInfo = false
+        })
+      }
+      this.licenceInfoError = null
+      const applianceId = ids[0]
+      const licenceInfo = await licenceSource.loadLicenceInfo(applianceId, opts && opts.skipLog)
       runInAction(() => {
-        this.licenceInfo = licence
+        this.licenceInfo = licenceInfo
         this.loadingLicenceInfo = false
       })
-    } catch (ex) {
+    } finally {
       runInAction(() => {
         this.loadingLicenceInfo = false
       })
-      throw ex
     }
   }
 
-  @action async addLicence(licence: string) {
+  @action async addLicence(licence: string, applianceId: string) {
     this.addingLicence = true
     try {
-      await licenceSource.addLicence(licence)
+      await licenceSource.addLicence(licence, applianceId)
       runInAction(() => {
         this.addingLicence = false
       })