Explorar o código

Improve waiting for schedule component requests

New loading animations are shown for different schedule component
operations.

The user now has an indication that a scheduling request is being
performed when adding, updating and removing a schedule item.
Sergiu Miclea %!s(int64=4) %!d(string=hai) anos
pai
achega
f3c64c18db

+ 62 - 24
src/components/molecules/ScheduleItem/ScheduleItem.tsx

@@ -32,6 +32,7 @@ import deleteImage from './images/delete.svg'
 import deleteHoverImage from './images/delete-hover.svg'
 import saveImage from './images/save.svg'
 import saveHoverImage from './images/save-hover.svg'
+import StatusIcon from '../../atoms/StatusIcon/StatusIcon'
 
 const Wrapper = styled.div<any>`
   display: flex;
@@ -42,6 +43,11 @@ const Wrapper = styled.div<any>`
     border-bottom: 1px solid ${Palette.grayscale[1]};
   }
 `
+const EnablingIcon = styled.div`
+  position: absolute;
+  top: 24px;
+  left: 8px;
+`
 const Data = styled.div<any>`
   width: ${props => props.width};
 `
@@ -87,6 +93,16 @@ const SaveButton = styled.div<any>`
     background: url('${saveHoverImage}') center no-repeat;
   }
 `
+const SavingIcon = styled.div`
+  position: absolute;
+  right: -64px;
+  top: 24px;
+`
+const DeletingIcon = styled.div`
+  position: absolute;
+  right: -32px;
+  top: 24px;
+`
 const padNumber = (number: number) => {
   if (number < 10) return `0${number}`
   return number.toString()
@@ -103,6 +119,9 @@ type Props = {
   onDeleteClick: () => void,
   unsavedSchedules: Schedule[],
   timezone: TimezoneValue,
+  saving: boolean
+  enabling: boolean
+  deleting: boolean
 }
 @observer
 class ScheduleItem extends React.Component<Props> {
@@ -200,7 +219,7 @@ class ScheduleItem extends React.Component<Props> {
       items.push({ label, value: value + 1 })
     })
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       return this.renderLabel(this.getFieldValue(items, 'month'))
     }
 
@@ -225,7 +244,7 @@ class ScheduleItem extends React.Component<Props> {
       items.push({ label: i.toString(), value: i })
     }
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       return this.renderLabel(this.getFieldValue(items, 'dom'))
     }
 
@@ -250,7 +269,7 @@ class ScheduleItem extends React.Component<Props> {
       items.push({ label, value })
     })
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       return this.renderLabel(this.getFieldValue(items, 'dow', true))
     }
 
@@ -273,7 +292,7 @@ class ScheduleItem extends React.Component<Props> {
       items.push({ label: padNumber(i), value: i })
     }
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       return this.renderLabel(this.getFieldValue(items, 'hour', true, 1))
     }
 
@@ -296,7 +315,7 @@ class ScheduleItem extends React.Component<Props> {
       items.push({ label: padNumber(i), value: i })
     }
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       return this.renderLabel(this.getFieldValue(items, 'minute', true, 1))
     }
 
@@ -316,7 +335,7 @@ class ScheduleItem extends React.Component<Props> {
   renderExpirationValue() {
     const date = this.props.item.expiration_date && moment(this.props.item.expiration_date)
 
-    if (this.props.item.enabled) {
+    if (this.props.item.enabled || this.props.deleting) {
       let labelDate = date
       if (this.props.timezone === 'utc' && date) {
         labelDate = DateUtils.getUtcTime(date)
@@ -340,13 +359,20 @@ class ScheduleItem extends React.Component<Props> {
     return (
       <Wrapper data-test-id="scheduleItem">
         <Data width={this.props.colWidths[0]}>
-          <Switch
-            noLabel
-            height={16}
-            checked={enabled}
-            onChange={itemEnabled => { this.props.onChange({ enabled: itemEnabled }, true) }}
-            data-test-id="scheduleItem-enabled"
-          />
+          {this.props.enabling ? (
+            <EnablingIcon>
+              <StatusIcon status="RUNNING" />
+            </EnablingIcon>
+          ) : (
+            <Switch
+              noLabel
+              height={16}
+              disabled={this.props.deleting}
+              checked={enabled}
+              onChange={itemEnabled => { this.props.onChange({ enabled: itemEnabled }, true) }}
+              data-test-id="scheduleItem-enabled"
+            />
+          )}
         </Data>
         <Data width={this.props.colWidths[1]}>
           {this.renderMonthValue()}
@@ -381,17 +407,29 @@ class ScheduleItem extends React.Component<Props> {
           >•••
           </Button>
         </Data>
-        <DeleteButton
-          data-test-id="scheduleItem-deleteButton"
-          onClick={this.props.onDeleteClick}
-          hidden={this.props.item.enabled}
-        />
-        <SaveButton
-          data-test-id="scheduleItem-saveButton"
-          onClick={this.props.onSaveSchedule}
-          hidden={this.props.item.enabled
-            || !this.props.unsavedSchedules.find(us => us.id === this.props.item.id)}
-        />
+        {this.props.deleting ? (
+          <DeletingIcon>
+            <StatusIcon status="DELETING" />
+          </DeletingIcon>
+        ) : (
+          <DeleteButton
+            data-test-id="scheduleItem-deleteButton"
+            onClick={this.props.onDeleteClick}
+            hidden={this.props.item.enabled}
+          />
+        )}
+        {this.props.saving && !this.props.enabling ? (
+          <SavingIcon>
+            <StatusIcon status="RUNNING" />
+          </SavingIcon>
+        ) : (
+          <SaveButton
+            data-test-id="scheduleItem-saveButton"
+            onClick={this.props.onSaveSchedule}
+            hidden={this.props.item.enabled
+          || !this.props.unsavedSchedules.find(us => us.id === this.props.item.id)}
+          />
+        )}
       </Wrapper>
     )
   }

+ 3 - 0
src/components/molecules/ScheduleItem/story.tsx

@@ -21,6 +21,9 @@ const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
 const Wrapper = (props: any) => (
   <div style={{ width: '924px' }}>
     <ScheduleItem
+      saving={false}
+      enabling={false}
+      deleting={false}
       onChange={() => { }}
       onDeleteClick={() => { }}
       onSaveSchedule={() => { }}

+ 3 - 1
src/components/organisms/ReplicaDetailsContent/ReplicaDetailsContent.tsx

@@ -225,7 +225,9 @@ class ReplicaDetailsContent extends React.Component<Props, State> {
         onSaveSchedule={this.props.onScheduleSave}
         timezone={this.state.timezone}
         onTimezoneChange={timezone => { this.handleTimezoneChange(timezone) }}
-        data-test-id="rdContent-schedule"
+        savingIds={this.props.scheduleStore.savingIds}
+        enablingIds={this.props.scheduleStore.enablingIds}
+        deletingIds={this.props.scheduleStore.deletingIds}
       />
     )
   }

+ 27 - 16
src/components/organisms/Schedule/Schedule.tsx

@@ -33,6 +33,7 @@ import type { Field } from '../../../@types/Field'
 import { executionOptions } from '../../../constants'
 
 import scheduleImage from './images/schedule.svg'
+import LoadingButton from '../../molecules/LoadingButton/LoadingButton'
 
 const Wrapper = styled.div<any>`
   ${StyleProps.exactWidth(StyleProps.contentWidth)}
@@ -116,6 +117,9 @@ type Props = {
   onSaveSchedule?: (schedule: ScheduleType) => void,
   adding?: boolean,
   loading?: boolean,
+  savingIds?: string[],
+  enablingIds?: string[],
+  deletingIds?: string[],
   secondaryEmpty?: boolean,
 }
 type State = {
@@ -149,7 +153,7 @@ class Schedule extends React.Component<Props, State> {
 
   handleDeleteConfirmation() {
     this.setState({ showDeleteConfirmation: false })
-    if (this.state.selectedSchedule && this.state.selectedSchedule.id) {
+    if (this.state.selectedSchedule?.id) {
       this.props.onRemove(this.state.selectedSchedule.id)
     }
   }
@@ -275,7 +279,9 @@ class Schedule extends React.Component<Props, State> {
             }}
             onShowOptionsClick={() => { this.handleShowOptions(schedule) }}
             onDeleteClick={() => { this.handleDeleteClick(schedule) }}
-            data-test-id={`schedule-item-${schedule.id || ''}`}
+            saving={Boolean(this.props.savingIds?.find(id => id === schedule.id))}
+            enabling={Boolean(this.props.enablingIds?.find(id => id === schedule.id))}
+            deleting={Boolean(this.props.deletingIds?.find(id => id === schedule.id))}
           />
         ))}
       </Body>
@@ -303,14 +309,17 @@ class Schedule extends React.Component<Props, State> {
     return (
       <NoSchedules secondary={this.props.secondaryEmpty}>
         <ScheduleImage />
-        <NoSchedulesTitle data-test-id="schedule-noScheduleTitle">{this.props.secondaryEmpty ? 'Schedule this Replica' : 'This Replica has no Schedules.'}</NoSchedulesTitle>
+        <NoSchedulesTitle>{this.props.secondaryEmpty ? 'Schedule this Replica' : 'This Replica has no Schedules.'}</NoSchedulesTitle>
         <NoSchedulesSubtitle>{this.props.secondaryEmpty ? 'You can schedule this replica so that it executes automatically.' : 'Add a new schedule so that the Replica executes automatically.'}</NoSchedulesSubtitle>
-        <Button
-          hollow={this.props.secondaryEmpty}
-          onClick={() => { this.handleAddScheduleClick() }}
-          data-test-id="schedule-noScheduleAddButton"
-        >Add Schedule
-        </Button>
+        {this.props.adding ? (
+          <LoadingButton>Adding ...</LoadingButton>
+        ) : (
+          <Button
+            hollow={this.props.secondaryEmpty}
+            onClick={() => { this.handleAddScheduleClick() }}
+          >Add Schedule
+          </Button>
+        )}
       </NoSchedules>
     )
   }
@@ -329,13 +338,15 @@ class Schedule extends React.Component<Props, State> {
     return (
       <Footer>
         <Buttons>
-          <Button
-            data-test-id="schedule-addScheduleButton"
-            disabled={this.props.adding}
-            secondary
-            onClick={() => { this.handleAddScheduleClick() }}
-          >Add Schedule
-          </Button>
+          {this.props.adding ? (
+            <LoadingButton>Adding ...</LoadingButton>
+          ) : (
+            <Button
+              disabled={this.props.adding}
+              onClick={() => { this.handleAddScheduleClick() }}
+            >Add Schedule
+            </Button>
+          )}
         </Buttons>
         <Timezone>
           <TimezoneLabel>Show all times in</TimezoneLabel>

+ 45 - 21
src/stores/ScheduleStore.ts

@@ -42,6 +42,12 @@ class ScheduleStore {
 
   @observable adding: boolean = false
 
+  @observable savingIds: string[] = []
+
+  @observable enablingIds: string[] = []
+
+  @observable deletingIds: string[] = []
+
   @action async scheduleMultiple(replicaId: string, schedules: Schedule[]): Promise<void> {
     this.scheduling = true
 
@@ -84,10 +90,18 @@ class ScheduleStore {
   }
 
   @action async removeSchedule(replicaId: string, scheduleId: string): Promise<void> {
-    this.schedules = this.schedules.filter(s => s.id !== scheduleId)
-    this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== scheduleId)
-
-    await Source.removeSchedule(replicaId, scheduleId)
+    this.deletingIds.push(scheduleId)
+    try {
+      await Source.removeSchedule(replicaId, scheduleId)
+      runInAction(() => {
+        this.schedules = this.schedules.filter(s => s.id !== scheduleId)
+        this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== scheduleId)
+      })
+    } finally {
+      runInAction(() => {
+        this.deletingIds = this.deletingIds.filter(id => id !== scheduleId)
+      })
+    }
   }
 
   @action async updateSchedule(
@@ -98,9 +112,8 @@ class ScheduleStore {
     unsavedData?: Schedule | null,
     forceSave?: boolean,
   ): Promise<void> {
-    this.schedules = updateSchedule(this.schedules, scheduleId, data)
-
     if (!forceSave) {
+      this.schedules = updateSchedule(this.schedules, scheduleId, data)
       const unsavedSchedule = this.unsavedSchedules.find(s => s.id === scheduleId)
       if (unsavedSchedule) {
         this.unsavedSchedules = updateSchedule(this.unsavedSchedules, scheduleId, data)
@@ -109,22 +122,33 @@ class ScheduleStore {
       }
       return
     }
-    const schedule: Schedule = await Source.updateSchedule(
-      replicaId,
-      scheduleId,
-      data,
-      oldData,
-      unsavedData,
-    )
-    runInAction(() => {
-      this.schedules = this.schedules.map(s => {
-        if (s.id === schedule.id) {
-          return { ...schedule }
-        }
-        return { ...s }
+    this.savingIds.push(scheduleId)
+    if (data.enabled !== oldData?.enabled) {
+      this.enablingIds.push(scheduleId)
+    }
+    try {
+      const schedule: Schedule = await Source.updateSchedule(
+        replicaId,
+        scheduleId,
+        data,
+        oldData,
+        unsavedData,
+      )
+      runInAction(() => {
+        this.schedules = this.schedules.map(s => {
+          if (s.id === schedule.id) {
+            return { ...schedule }
+          }
+          return { ...s }
+        })
+        this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== schedule.id)
       })
-      this.unsavedSchedules = this.unsavedSchedules.filter(s => s.id !== schedule.id)
-    })
+    } finally {
+      runInAction(() => {
+        this.savingIds = this.savingIds.filter(id => id !== scheduleId)
+        this.enablingIds = this.enablingIds.filter(id => id !== scheduleId)
+      })
+    }
   }
 
   @action clearUnsavedSchedules() {