MetaPhase-Consulting/State-TalentMAP

View on GitHub
src/Components/AdministratorPage/Dashboard/DataSync/DataSync.jsx

Summary

Maintainability
A
0 mins
Test Coverage
B
86%
/*
TODO - server-side - next_synchronization is calculated server-side
using the last_synchronization + delta. We may want this field exposed for patching in the future
so that next_sync can be updated independently. Relevant components for this have been commented
out with "TODO server-side".
*/

import { Component } from 'react';
import PropTypes from 'prop-types';
import { get, isNull, omit, orderBy, startsWith, toString } from 'lodash';
import FA from 'react-fontawesome';
import { addHours, format, parse, subHours } from 'date-fns';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import { DateUtils } from 'react-day-picker';
import TimeInput from 'react-keyboard-time-input';
import InteractiveElement from '../../../InteractiveElement';
import Form from '../../../Form';
import FieldSet from '../../../FieldSet';
import RadioList from '../../../RadioList';
import Spinner from '../../../Spinner';
import { EMPTY_FUNCTION } from '../../../../Constants/PropTypes';
import { focusById } from '../../../../utilities';
import ExportButton from '../../../ExportButton';

export const FORMAT = 'MM-DD-YY hh:mm A';
export const FORMAT_TIME = 'hh:mm A';

export const formatDate = (date, format$ = FORMAT, locale) => format(date, format$, { locale });

export const parseDate = (str, format$, locale) => {
  if (str) {
    const parsed = parse(str, format$, { locale });
    if (DateUtils.isDate(parsed)) {
      return parsed;
    }
  }
  return undefined;
};

const getScheduledJob = (jobs = [], prop = 'next_synchronization', ordering = 'desc') => {
  let sync;
  if (jobs.length) {
    const orderedJobs = orderBy(jobs, prop, ordering);
    sync = get(orderedJobs, '[0]');
  }
  if (sync) {
    return { ...sync, next_synchronization: formatDate(sync.next_synchronization) };
  }
  return null;
};

class DataSync extends Component {
  constructor(props) {
    super(props);
    this.state = {
      showForm: false,
      formValues: {
        id: null,
        last_synchronization: null,
        delta_synchronization: 86400,
        running: 'false',
        talentmap_model: '',
        next_synchronization: null,
        priority: 0,
        use_last_date_updated: 'false',
      },
    };
  }

  setJob = job => {
    const job$ = {
      ...job,
      last_synchronization: parseDate(new Date(job.last_synchronization)),
      last_synchronization_time: format(job.last_synchronization, FORMAT_TIME),

      // TODO - server-side
      // next_synchronization: parseDate(new Date(job.next_synchronization)),
      // next_synchronization_time: format(job.next_synchronization, FORMAT_TIME),

      running: job.running ? 'true' : 'false',
      use_last_date_updated: job.use_last_date_updated ? 'true' : 'false',
    };
    this.setState({ formValues: job$ }, () => this.showForm());
  };

  submitSync = () => {
    const { patchSyncJob } = this.props;
    const { formValues: f } = this.state;

    // Combine date with time since they're separated into two form fields, but
    // one on the API.
    const parseDateTime = (date, time) => {
      let date$ = parse(
        `${format(date, 'YYYY-MM-DD')} ${time}`,
        `YYYY-MM-DD ${FORMAT_TIME}`,
      );
      // date-fns can format TO am/pm but can't parse FROM it,
      // so we check whether or not we need to add an extra 12 hours
      // by looking for PM/pm in the time string.
      // This might be possible in date-fns 2.0, but it's in beta
      // and has breaking changes.
      const isPM = time.includes('PM') || time.includes('pm');
      const is12 = startsWith(time, '12:');
      if (isPM && !is12) {
        date$ = addHours(date$, 12);
      }
      if (!isPM && is12) {
        date$ = subHours(date$, 12);
      }
      return date$;
    };

    const formValues$ = {
      ...omit(f, ['next_synchronization', 'last_synchronization_time']), // TODO - server-side
      last_synchronization: parseDateTime(f.last_synchronization, f.last_synchronization_time),
      running: f.running === 'true',
      use_last_date_updated: f.use_last_date_updated === 'true',
    };

    patchSyncJob(formValues$);
  };

  showForm = () => {
    this.setState({ showForm: true });
    focusById('form-header', 1);
  };

  closeForm = () => {
    this.setState({ showForm: false });
    focusById('new-sync-button', 1);
  };

  updateTime = (value, field = 'next_synchronization_time') => {
    const { formValues } = this.state;
    formValues[field] = value;
    this.setState({ formValues });
  };

  updateBool = (value = false, field) => {
    const { formValues } = this.state;
    if (field) {
      formValues[field] = toString(value);
      this.setState({ formValues });
    }
  };

  updateVal = (value = null, field) => {
    const v$ = get(value, 'target.value');
    const { formValues } = this.state;
    if (!isNull(v$)) {
      formValues[field] = v$;
      this.setState({ formValues });
    }
  };

  updateDate = (value, field = 'next_synchronization') => {
    const { formValues } = this.state;
    formValues[field] = value;
    this.setState({ formValues });
  };

  dateIsValid = (field = 'next_synchronization') => {
    const { formValues: { [field]: field$ } } = this.state;
    return !!field$;
  };

  formIsValid = () => this.dateIsValid('next_synchronization') && this.dateIsValid('last_synchronization');

  render() {
    const { showForm, formValues } = this.state;
    const { syncJobs, isLoading, patchSyncIsLoading, runAllJobs } = this.props;
    const formIsValid = this.formIsValid();
    // const dateIsValid = this.dateIsValid('next_synchronization'); TODO server-side
    const dateLastIsValid = this.dateIsValid('last_synchronization');
    const nextScheduled = getScheduledJob(syncJobs);
    const lastRun = getScheduledJob(syncJobs, 'last_synchronization');
    return (
      <div className="usa-grid-full padded-section data-sync-section">
        <h3>TalentMAP Data Sync</h3>
        {
          isLoading &&
            <Spinner type="homepage-position-results" size="big" />
        }
        {!isLoading &&
          <div>
            <div className="usa-grid-full top-section">
              {
                nextScheduled &&
                <div>
                  <strong>Next sync scheduled: </strong>
                  <span>{nextScheduled.talentmap_model}: {nextScheduled.next_synchronization}</span>
                </div>
              }
              <div className="usa-grid-full sync-job-container">
                {
                  syncJobs.map((n) => {
                    const nextSyncDate = format(n.next_synchronization, FORMAT);
                    return (
                      <div key={n.id} className="usa-grid-full sync-job-item">
                        <div>
                          <strong>{n.talentmap_model}: </strong>
                          <span>Next sync at {nextSyncDate}</span>
                        </div>
                        <InteractiveElement title="Edit details for this job" type="span" onClick={() => this.setJob(n)}>
                          <FA name="pencil" />
                        </InteractiveElement>
                      </div>
                    );
                  })
                }
              </div>
              {
                !showForm &&
                  <div className="usa-grid-full new-sync-container">
                    Click the pencil next to one of the jobs above to edit its details.
                  </div>
              }
              {
                showForm &&
                <div className="usa-grid-full new-sync-container new-sync-container--form">
                  <div className="usa-grid-full new-sync-form">
                    <h4 id="form-header" tabIndex="-1">Update Sync Details</h4>
                    <Form id="sync-form" onFormSubmit={(e) => { e.preventDefault(); }}>
                      <FieldSet legend="Last Synchronization">
                        <div className="date-time-forms">
                          <div className={`date-time-form date-time-form--date ${dateLastIsValid ? '' : 'usa-input-error'}`}>
                            <label
                              htmlFor={`day-picker-input ${dateLastIsValid ? 'input-type-text' : 'usa-input-error-label'}`}
                              className="label"
                            >
                              Date:
                            </label>
                            {!dateLastIsValid && <span className="usa-input-error-message usa-sr-only" id="input-error-message" role="alert">Must be a valid, present or future date</span>}
                            <DayPickerInput
                              containerProps={{ id: 'day-picker-input', ariaDescribedBy: 'input-error-message' }}
                              onDayChange={e => this.updateDate(e, 'last_synchronization')}
                              value={formValues.last_synchronization}
                              format={'MM/DD/YYYY'}
                              formatDate={formatDate}
                              parseDate={parseDate}
                            />
                          </div>
                          <div className="date-time-form date-time-form--time">
                            <label htmlFor="time-input" className="label">Time:</label>
                            <TimeInput
                              id="time-input"
                              value={formValues.last_synchronization_time}
                              onChange={e => this.updateTime(e, 'last_synchronization_time')}
                            />
                          </div>
                        </div>
                      </FieldSet>

                      {
                        /* eslint-disable */
                       /* TODO - server-side
                      <FieldSet legend="Next Synchronization">
                        <div className="date-time-forms">
                          <div className={`date-time-form date-time-form--date ${dateIsValid ? '' : 'usa-input-error'}`}>
                            <label
                              htmlFor={`day-picker-input ${dateIsValid ? 'input-type-text' : 'usa-input-error-label'}`}
                              className="label"
                            >
                              Date:
                            </label>
                            {!dateIsValid && <span className="usa-input-error-message usa-sr-only" id="input-error-message" role="alert">Must be a valid, present or future date</span>}
                            <DayPickerInput
                              containerProps={{ id: 'day-picker-input', ariaDescribedBy: 'input-error-message' }}
                              onDayChange={e => this.updateDate(e, 'next_synchronization')}
                              value={formValues.next_synchronization}
                              format={'MM/DD/YYYY'}
                              formatDate={formatDate}
                              parseDate={parseDate}
                            />
                          </div>
                          <div className="date-time-form date-time-form--time">
                            <label htmlFor="time-input" className="label">Time:</label>
                            <TimeInput
                              id="time-input"
                              value={formValues.next_synchronization_time}
                              onChange={e => this.updateTime(e, 'next_synchronization_time')}
                            />
                          </div>
                        </div>
                      </FieldSet>
                      */
                      /* eslint-enable */
                      }

                      <FieldSet legend="TalentMAP Model">
                        <input type="string" value={formValues.talentmap_model} onChange={e => this.updateVal(e, 'talentmap_model')} />
                      </FieldSet>
                      <FieldSet legend="Priority">
                        <input type="number" value={formValues.priority} onChange={e => this.updateVal(e, 'priority')} />
                      </FieldSet>
                      <FieldSet legend="Delta">
                        <input type="number" value={formValues.delta_synchronization} onChange={e => this.updateVal(e, 'delta_synchronization')} />
                      </FieldSet>
                      <FieldSet legend="Use last updated date">
                        <RadioList
                          options={[
                            { id: 'lastdate-true', value: 'true', label: 'True' },
                            { id: 'lastdate-false', value: 'false', label: 'False' },
                          ]}
                          value={formValues.use_last_date_updated}
                          onChange={e => this.updateBool(e, 'use_last_date_updated')}
                        />
                      </FieldSet>
                      <FieldSet legend="Running">
                        <RadioList
                          options={[
                            { id: 'running-true', value: 'true', label: 'True' },
                            { id: 'running-false', value: 'false', label: 'False' },
                          ]}
                          value={formValues.running}
                          onChange={e => this.updateBool(e, 'running')}
                        />
                      </FieldSet>
                    </Form>
                    <div className="export-button-container">
                      <ExportButton
                        disabled={!formIsValid}
                        type="submit"
                        form="sync-form"
                        value="Update sync"
                        onClick={this.submitSync}
                        isLoading={patchSyncIsLoading}
                        text="Update sync"
                      />
                    </div>
                    <button
                      title="Close form"
                      className="feedback-close unstyled-button"
                      onClick={this.closeForm}
                    >
                      <FA name="times" />
                      <span className="usa-sr-only">Close Feedback</span>
                    </button>
                  </div>
                </div>
              }
            </div>
            <div className="usa-grid-full status-container">
              <div className="usa-grid-full status-container--inner">
                { lastRun && <div>
                  <strong>Last sync: </strong>
                  <span>{nextScheduled.talentmap_model}: {nextScheduled.next_synchronization}</span>
                </div> }
              </div>
              <button className="usa-button-secondary" onClick={runAllJobs}>Run sync now</button>
            </div>
          </div>
        }
      </div>
    );
  }
}

DataSync.propTypes = {
  syncJobs: PropTypes.arrayOf(PropTypes.shape({})),
  isLoading: PropTypes.bool,
  runAllJobs: PropTypes.func,
  patchSyncIsLoading: PropTypes.bool,
  patchSyncJob: PropTypes.func,
};

DataSync.defaultProps = {
  syncJobs: [],
  isLoading: false,
  runAllJobs: EMPTY_FUNCTION,
  patchSyncIsLoading: false,
  patchSyncJob: EMPTY_FUNCTION,
};

export default DataSync;