فهرست منبع

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 سال پیش
والد
کامیت
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 { observer } from "mobx-react";
 import CopyValue from "@src/components/ui/CopyValue";
 import CopyValue from "@src/components/ui/CopyValue";
 import Button from "@src/components/ui/Button";
 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";
 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;
   align-items: center;
   justify-content: flex-end;
   justify-content: flex-end;
-  margin-top: -70px;
+  margin-top: -66px;
   margin-bottom: 32px;
   margin-bottom: 32px;
+  margin-left: 320px;
+  transition: all ${ThemeProps.animations.swift};
 `;
 `;
 const FingerPrint = styled.div`
 const FingerPrint = styled.div`
   margin-right: 32px;
   margin-right: 32px;
@@ -43,6 +45,7 @@ type Props = {
   hideButton: boolean;
   hideButton: boolean;
   error: string;
   error: string;
   fingerprint: string;
   fingerprint: string;
+  visible: boolean;
   onCreateClick: () => void;
   onCreateClick: () => void;
 };
 };
 
 
@@ -74,7 +77,7 @@ class MetalHubListHeader extends React.Component<Props> {
 
 
   render() {
   render() {
     return (
     return (
-      <Wrapper>
+      <Wrapper visible={this.props.visible}>
         {this.renderContent()}
         {this.renderContent()}
         {!this.props.hideButton ? (
         {!this.props.hideButton ? (
           <Button hollow onClick={this.props.onCreateClick}>
           <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 moment from "moment";
 import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
 import StatusPill from "@src/components/ui/StatusComponents/StatusPill";
 import serverImage from "./images/server.svg";
 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>`
 const Content = styled.div<any>`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+  margin-left: 16px;
   border-top: 1px solid ${ThemePalette.grayscale[1]};
   border-top: 1px solid ${ThemePalette.grayscale[1]};
   padding: 8px 16px;
   padding: 8px 16px;
   cursor: pointer;
   cursor: pointer;
@@ -41,6 +47,10 @@ const Wrapper = styled.div<any>`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
 
 
+  &:hover ${CheckboxStyled} {
+    opacity: 1;
+  }
+
   &:last-child ${Content} {
   &:last-child ${Content} {
     border-bottom: 1px solid ${ThemePalette.grayscale[1]};
     border-bottom: 1px solid ${ThemePalette.grayscale[1]};
   }
   }
@@ -77,7 +87,7 @@ const Body = styled.div`
   display: flex;
   display: flex;
 `;
 `;
 const Data = styled.div<{ width: number }>`
 const Data = styled.div<{ width: number }>`
-  width: ${props => props.width}px;
+  min-width: ${props => props.width}px;
   margin: 0 32px;
   margin: 0 32px;
 
 
   &:last-child {
   &:last-child {
@@ -87,6 +97,8 @@ const Data = styled.div<{ width: number }>`
 
 
 type Props = {
 type Props = {
   item: MetalHubServer;
   item: MetalHubServer;
+  selected: boolean;
+  onSelectedChange: (value: boolean) => void;
   onClick: () => void;
   onClick: () => void;
 };
 };
 @observer
 @observer
@@ -94,6 +106,10 @@ class MetalHubServerListItem extends React.Component<Props> {
   render() {
   render() {
     return (
     return (
       <Wrapper>
       <Wrapper>
+        <CheckboxStyled
+          checked={this.props.selected}
+          onChange={this.props.onSelectedChange}
+        />
         <Content onClick={this.props.onClick}>
         <Content onClick={this.props.onClick}>
           <Image />
           <Image />
           <Title>
           <Title>
@@ -113,7 +129,7 @@ class MetalHubServerListItem extends React.Component<Props> {
             )}
             )}
           </Title>
           </Title>
           <Body>
           <Body>
-            <Data width={500}>
+            <Data width={210}>
               <ItemLabel>API Endpoint</ItemLabel>
               <ItemLabel>API Endpoint</ItemLabel>
               <ItemValue>{this.props.item.api_endpoint}</ItemValue>
               <ItemValue>{this.props.item.api_endpoint}</ItemValue>
             </Data>
             </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 projectStore from "@src/stores/ProjectStore";
 import MetalHubModal from "@src/components/modules/MetalHubModule/MetalHubModal";
 import MetalHubModal from "@src/components/modules/MetalHubModule/MetalHubModal";
 import emptyListImage from "./images/server.svg";
 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 Wrapper = styled.div``;
 const ErrorWrapper = styled.div`
 const ErrorWrapper = styled.div`
@@ -48,12 +56,16 @@ const ErrorMessage = styled.div`
 type State = {
 type State = {
   modalIsOpen: boolean;
   modalIsOpen: boolean;
   showNewServerModal: boolean;
   showNewServerModal: boolean;
+  selectedServers: number[];
+  showConfirmRemove: boolean;
 };
 };
 @observer
 @observer
 class MetalHubServersPage extends React.Component<{ history: any }, State> {
 class MetalHubServersPage extends React.Component<{ history: any }, State> {
-  state = {
+  state: State = {
     modalIsOpen: false,
     modalIsOpen: false,
     showNewServerModal: false,
     showNewServerModal: false,
+    selectedServers: [],
+    showConfirmRemove: false,
   };
   };
 
 
   pollTimeout = 0;
   pollTimeout = 0;
@@ -100,10 +112,69 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
     await metalHubStore.getServers();
     await metalHubStore.getServers();
   }
   }
 
 
+  handleRemoveAction() {
+    this.setState({ showConfirmRemove: true });
+  }
+
   handleProjectChange() {
   handleProjectChange() {
     metalHubStore.getServers({ showLoading: true });
     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) {
   async pollData(showLoading?: boolean) {
     if (this.state.modalIsOpen || this.stopPolling) {
     if (this.state.modalIsOpen || this.stopPolling) {
       return;
       return;
@@ -153,11 +224,45 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
   }
   }
 
 
   render() {
   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 (
     return (
       <Wrapper>
       <Wrapper>
         <MainTemplate
         <MainTemplate
           navigationComponent={<Navigation currentPage="bare-metal-servers" />}
           navigationComponent={<Navigation currentPage="bare-metal-servers" />}
-          listNoMargin
           listComponent={
           listComponent={
             <FilterList
             <FilterList
               filterItems={[
               filterItems={[
@@ -165,11 +270,18 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
                 { label: "Active", value: "active" },
                 { label: "Active", value: "active" },
                 { label: "Inactive", value: "inactive" },
                 { label: "Inactive", value: "inactive" },
               ]}
               ]}
-              selectionLabel=""
+              dropdownActions={bulkActions}
+              selectionLabel="server"
               loading={metalHubStore.loadingServers}
               loading={metalHubStore.loadingServers}
               items={metalHubStore.servers}
               items={metalHubStore.servers}
+              onSelectedItemsChange={selectedServers => {
+                this.setState({
+                  selectedServers: selectedServers.map(s => s.id),
+                });
+              }}
               listHeaderComponent={
               listHeaderComponent={
                 <MetalHubListHeader
                 <MetalHubListHeader
+                  visible={this.state.selectedServers.length === 0}
                   fingerprint={metalHubStore.fingerprint}
                   fingerprint={metalHubStore.fingerprint}
                   error={metalHubStore.loadingFingerprintError}
                   error={metalHubStore.loadingFingerprintError}
                   hideButton={metalHubStore.servers.length === 0}
                   hideButton={metalHubStore.servers.length === 0}
@@ -188,8 +300,8 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
                 this.handleReloadButtonClick();
                 this.handleReloadButtonClick();
               }}
               }}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
               itemFilterFunction={(...args) => this.itemFilterFunction(...args)}
-              renderItemComponent={component => (
-                <MetalHubServerListItem {...component} />
+              renderItemComponent={props => (
+                <MetalHubServerListItem {...props} />
               )}
               )}
               emptyListImage={emptyListImage}
               emptyListImage={emptyListImage}
               emptyListComponent={this.renderEmptyListComponent()}
               emptyListComponent={this.renderEmptyListComponent()}
@@ -227,6 +339,20 @@ class MetalHubServersPage extends React.Component<{ history: any }, State> {
             }}
             }}
           />
           />
         ) : null}
         ) : 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>
       </Wrapper>
     );
     );
   }
   }

+ 11 - 1
src/sources/MetalHubSource.ts

@@ -46,7 +46,17 @@ class MetalHubSource {
       skipLog,
       skipLog,
       quietError: true,
       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> {
   async getServerDetails(serverId: number): Promise<MetalHubServer> {