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

Improve Metal Hub Servers list page

Adds the ability to perform bulk create replica, create migration,
remove, and refresh actions by enabling multiple server selection.

Resolves column sizing issues in server list item layout.

Sorts servers by "updated at" field for better organization.
Sergiu Miclea 3 лет назад
Родитель
Сommit
f3e64874c6

+ 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> {