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

Merge pull request #743 from smiclea/improve-hub

Improve Metal Hub Servers list page
Daniel Vincze 3 лет назад
Родитель
Сommit
1b0e022c9d

+ 8 - 5
src/components/modules/MetalHubModule/MetalHubListHeader/MetalHubListHeader.tsx

@@ -17,15 +17,17 @@ import styled from "styled-components";
 import { observer } from "mobx-react";
 import CopyValue from "@src/components/ui/CopyValue";
 import Button from "@src/components/ui/Button";
-import { ThemePalette } from "@src/components/Theme";
+import { ThemePalette, ThemeProps } from "@src/components/Theme";
 import InfoIcon from "@src/components/ui/InfoIcon";
 
-const Wrapper = styled.div`
-  display: flex;
+const Wrapper = styled.div<{ visible: boolean }>`
+  display: ${props => (props.visible ? "flex" : "none")};
   align-items: center;
   justify-content: flex-end;
-  margin-top: -70px;
+  margin-top: -66px;
   margin-bottom: 32px;
+  margin-left: 320px;
+  transition: all ${ThemeProps.animations.swift};
 `;
 const FingerPrint = styled.div`
   margin-right: 32px;
@@ -43,6 +45,7 @@ type Props = {
   hideButton: boolean;
   error: string;
   fingerprint: string;
+  visible: boolean;
   onCreateClick: () => void;
 };
 
@@ -74,7 +77,7 @@ class MetalHubListHeader extends React.Component<Props> {
 
   render() {
     return (
-      <Wrapper>
+      <Wrapper visible={this.props.visible}>
         {this.renderContent()}
         {!this.props.hideButton ? (
           <Button hollow onClick={this.props.onCreateClick}>

+ 18 - 2
src/components/modules/MetalHubModule/MetalHubListItem/MetalHubListItem.tsx

@@ -22,10 +22,16 @@ import { MetalHubServer } from "@src/@types/MetalHub";
 import moment from "moment";
 import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
 import serverImage from "./images/server.svg";
+import Checkbox from "@src/components/ui/Checkbox";
 
+const CheckboxStyled = styled(Checkbox)`
+  opacity: ${props => (props.checked ? 1 : 0)};
+  transition: all ${ThemeProps.animations.swift};
+`;
 const Content = styled.div<any>`
   display: flex;
   align-items: center;
+  margin-left: 16px;
   border-top: 1px solid ${ThemePalette.grayscale[1]};
   padding: 8px 16px;
   cursor: pointer;
@@ -41,6 +47,10 @@ const Wrapper = styled.div<any>`
   display: flex;
   align-items: center;
 
+  &:hover ${CheckboxStyled} {
+    opacity: 1;
+  }
+
   &:last-child ${Content} {
     border-bottom: 1px solid ${ThemePalette.grayscale[1]};
   }
@@ -77,7 +87,7 @@ const Body = styled.div`
   display: flex;
 `;
 const Data = styled.div<{ width: number }>`
-  width: ${props => props.width}px;
+  min-width: ${props => props.width}px;
   margin: 0 32px;
 
   &:last-child {
@@ -87,6 +97,8 @@ const Data = styled.div<{ width: number }>`
 
 type Props = {
   item: MetalHubServer;
+  selected: boolean;
+  onSelectedChange: (value: boolean) => void;
   onClick: () => void;
 };
 @observer
@@ -94,6 +106,10 @@ class MetalHubServerListItem extends React.Component<Props> {
   render() {
     return (
       <Wrapper>
+        <CheckboxStyled
+          checked={this.props.selected}
+          onChange={this.props.onSelectedChange}
+        />
         <Content onClick={this.props.onClick}>
           <Image />
           <Title>
@@ -113,7 +129,7 @@ class MetalHubServerListItem extends React.Component<Props> {
             )}
           </Title>
           <Body>
-            <Data width={500}>
+            <Data width={210}>
               <ItemLabel>API Endpoint</ItemLabel>
               <ItemValue>{this.props.item.api_endpoint}</ItemValue>
             </Data>

+ 131 - 5
src/components/smart/MetalHubServersPage/MetalHubServersPage.tsx

@@ -30,6 +30,14 @@ import MetalHubListHeader from "@src/components/modules/MetalHubModule/MetalHubL
 import projectStore from "@src/stores/ProjectStore";
 import MetalHubModal from "@src/components/modules/MetalHubModule/MetalHubModal";
 import emptyListImage from "./images/server.svg";
+import type { DropdownAction } from "@src/components/ui/Dropdowns/ActionDropdown";
+import AlertModal from "@src/components/ui/AlertModal";
+import { ThemePalette } from "@src/components/Theme";
+import instanceSource from "@src/sources/InstanceSource";
+import instanceStore from "@src/stores/InstanceStore";
+import notificationStore from "@src/stores/NotificationStore";
+import { WizardData } from "@src/@types/WizardData";
+import { wizardPages } from "@src/constants";
 
 const Wrapper = styled.div``;
 const ErrorWrapper = styled.div`
@@ -48,12 +56,16 @@ const ErrorMessage = styled.div`
 type State = {
   modalIsOpen: boolean;
   showNewServerModal: boolean;
+  selectedServers: number[];
+  showConfirmRemove: boolean;
 };
 @observer
 class MetalHubServersPage extends React.Component<{ history: any }, State> {
-  state = {
+  state: State = {
     modalIsOpen: false,
     showNewServerModal: false,
+    selectedServers: [],
+    showConfirmRemove: false,
   };
 
   pollTimeout = 0;
@@ -100,10 +112,69 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
     await metalHubStore.getServers();
   }
 
+  handleRemoveAction() {
+    this.setState({ showConfirmRemove: true });
+  }
+
   handleProjectChange() {
     metalHubStore.getServers({ showLoading: true });
   }
 
+  async removeSelectedServers() {
+    this.setState({ showConfirmRemove: false });
+    await Promise.all(
+      this.state.selectedServers.map(async serverId => {
+        await metalHubStore.deleteServer(serverId);
+      })
+    );
+    metalHubStore.getServers({ showLoading: true });
+  }
+
+  async refreshServers() {
+    await Promise.all(
+      this.state.selectedServers.map(async serverId => {
+        await metalHubStore.refreshServer(serverId);
+      })
+    );
+    metalHubStore.getServers({ showLoading: true });
+  }
+
+  async create(type: "replica" | "migration") {
+    notificationStore.alert(`Preparing ${type}...`, "info");
+    const { selectedServers } = this.state;
+    const endpoint = await metalHubStore.getMetalHubEndpoint();
+
+    // Remove the instances from cache so that the wizard has the latest data before it's shown
+    instanceSource.removeInstancesFromCache(endpoint.id);
+    await instanceStore.loadInstancesInChunks({
+      endpoint,
+      vmsPerPage: Infinity,
+    });
+    // find all the selected servers in the background instances
+    const instances = instanceStore.backgroundInstances.filter(
+      i => selectedServers.map(s => String(s)).indexOf(i.id) > -1
+    );
+    if (instances.length !== selectedServers.length) {
+      notificationStore.alert(
+        `Could not find all instances on endpoint '${endpoint.name}'`,
+        "error"
+      );
+      throw new Error("Instances not found");
+    }
+    const data: WizardData = {
+      source: endpoint,
+      selectedInstances: instances,
+    };
+    this.props.history.push(
+      `/wizard/${type}/?d=${window.btoa(
+        JSON.stringify({
+          data,
+          currentPage: wizardPages.find(p => p.id === "target"),
+        })
+      )}`
+    );
+  }
+
   async pollData(showLoading?: boolean) {
     if (this.state.modalIsOpen || this.stopPolling) {
       return;
@@ -153,11 +224,45 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
   }
 
   render() {
+    const selectedServers = metalHubStore.servers.filter(s =>
+      this.state.selectedServers.includes(s.id)
+    );
+    const bulkActions: DropdownAction[] = [
+      {
+        label: "Create Replica",
+        color: ThemePalette.primary,
+        disabled: !selectedServers.every(s => s.active),
+        action: () => {
+          this.create("replica");
+        },
+      },
+      {
+        label: "Create Migration",
+        color: ThemePalette.primary,
+        disabled: !selectedServers.every(s => s.active),
+        action: () => {
+          this.create("migration");
+        },
+      },
+      {
+        label: "Refresh Servers",
+        action: () => {
+          this.refreshServers();
+        },
+      },
+      {
+        label: "Remove Servers",
+        color: ThemePalette.alert,
+        action: () => {
+          this.handleRemoveAction();
+        },
+      },
+    ];
+
     return (
       <Wrapper>
         <MainTemplate
           navigationComponent={<Navigation currentPage="bare-metal-servers" />}
-          listNoMargin
           listComponent={
             <FilterList
               filterItems={[
@@ -165,11 +270,18 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
                 { label: "Active", value: "active" },
                 { label: "Inactive", value: "inactive" },
               ]}
-              selectionLabel=""
+              dropdownActions={bulkActions}
+              selectionLabel="server"
               loading={metalHubStore.loadingServers}
               items={metalHubStore.servers}
+              onSelectedItemsChange={selectedServers => {
+                this.setState({
+                  selectedServers: selectedServers.map(s => s.id),
+                });
+              }}
               listHeaderComponent={
                 <MetalHubListHeader
+                  visible={this.state.selectedServers.length === 0}
                   fingerprint={metalHubStore.fingerprint}
                   error={metalHubStore.loadingFingerprintError}
                   hideButton={metalHubStore.servers.length === 0}
@@ -188,8 +300,8 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
                 this.handleReloadButtonClick();
               }}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
-              renderItemComponent={component => (
-                <MetalHubServerListItem {...component} />
+              renderItemComponent={props => (
+                <MetalHubServerListItem {...props} />
               )}
               emptyListImage={emptyListImage}
               emptyListComponent={this.renderEmptyListComponent()}
@@ -227,6 +339,20 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
             }}
           />
         ) : null}
+        {this.state.showConfirmRemove ? (
+          <AlertModal
+            isOpen
+            title="Remove Selected Bare Metal Servers?"
+            message="Are you sure you want to remove the selected Coriolis Bare Metal Servers?"
+            extraMessage="&nbsp;"
+            onConfirmation={() => {
+              this.removeSelectedServers();
+            }}
+            onRequestClose={() => {
+              this.setState({ showConfirmRemove: false });
+            }}
+          />
+        ) : null}
       </Wrapper>
     );
   }

+ 11 - 1
src/sources/MetalHubSource.ts

@@ -46,7 +46,17 @@ class MetalHubSource {
       skipLog,
       quietError: true,
     });
-    return response.data;
+    const servers: MetalHubServer[] = response.data;
+    servers.sort((a, b) => {
+      if (new Date(a.updated_at) > new Date(b.updated_at)) {
+        return -1;
+      }
+      if (new Date(a.updated_at) < new Date(b.updated_at)) {
+        return 1;
+      }
+      return 0;
+    });
+    return servers;
   }
 
   async getServerDetails(serverId: number): Promise<MetalHubServer> {