<commit_before>import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { get, isEmpty, map } from 'lodash';

import './ProjectDetails.styl';
import Spinner from './Spinner';

export default class ProjectDetails extends Component {
  constructor(props, context) {
    super(props, context);
    this.onDeploy = this.onDeploy.bind(this);
  }

  componentWillMount() {
    this.props.fetchProject(this.props.project.id);
  }

  componentDidMount() {
    this.interval = window.setInterval(() => {
      if (this.props.project.lastDeploy && this.props.project.lastDeploy.finishedAt === null) {
        this.props.fetchProject(this.props.project.id);
      }
    }, 500);
  }

  componentWillUnmount() {
    if (this.interval) {
      window.clearInterval(this.interval);
    }
  }

  onDeploy() {
    this.props.deployProject(this.props.project.id, {});
  }

  render() {
    const { lastDeploy } = this.props.project;
    return (
      <div className="ProjectDetails">
        <h1>
          <button onClick={this.onDeploy}>Deploy</button>
          <span>{get(this.props.project.owner, 'name')} / {this.props.project.name}</span>
          {lastDeploy.finishedAt === null && <Spinner />}
        </h1>
        {lastDeploy &&
          <div>
            <h2>Last deployment</h2>
            {lastDeploy && <em>
              {get(lastDeploy, 'shortRef')} - {moment(get(lastDeploy, 'finishedAt')).fromNow()}
            </em>}
            {map(lastDeploy.taskResults, (task, i) => <TaskResult key={i} {...task} />)}
          </div>
        }
      </div>
    );
  }
}

ProjectDetails.propTypes = {
  fetchProject: PropTypes.func.isRequired,
  deployProject: PropTypes.func.isRequired,
  project: PropTypes.object.isRequired,
};

const TaskResult = ({ commands, taskType, result = {} }) => {
  let keys = Object.keys(result);
  if (!isEmpty(commands) && !result.hasOwnProperty('ssh')) {
    keys = commands;
  }

  return (
    <div>
      <h3>{taskType}</h3>
      <code className="overflow-x">
        {map(keys, key => {
          if (!result[key]) return false;
          const { error, stdout, exitCode } = result[key];
          return (
            <div key={taskType + key} className="padding">
              $ <strong>{key}</strong> <span title="Exit code">({exitCode})</span>
              <pre>{stdout || error}</pre>
            </div>
          );
        })}
      </code>
    </div>
  );
};

TaskResult.propTypes = {
  commands: PropTypes.array.isRequired,
  taskType: PropTypes.string.isRequired,
  result: PropTypes.object.isRequired,
};<commit_msg>fix: Add better support for empty results<commit_after>import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { get, isEmpty, keys, map } from 'lodash';

import './ProjectDetails.styl';
import Spinner from './Spinner';

export default class ProjectDetails extends Component {
  constructor(props, context) {
    super(props, context);
    this.onDeploy = this.onDeploy.bind(this);
  }

  componentWillMount() {
    this.props.fetchProject(this.props.project.id);
  }

  componentDidMount() {
    this.interval = window.setInterval(() => {
      if (this.props.project.lastDeploy && this.props.project.lastDeploy.finishedAt === null) {
        this.props.fetchProject(this.props.project.id);
      }
    }, 500);
  }

  componentWillUnmount() {
    if (this.interval) {
      window.clearInterval(this.interval);
    }
  }

  onDeploy() {
    this.props.deployProject(this.props.project.id, {});
  }

  render() {
    const { lastDeploy } = this.props.project;
    return (
      <div className="ProjectDetails">
        <h1>
          <button onClick={this.onDeploy}>Deploy</button>
          <span>{get(this.props.project.owner, 'name')} / {this.props.project.name}</span>
          {lastDeploy.finishedAt === null && <Spinner />}
        </h1>
        {lastDeploy &&
          <div>
            <h2>Last deployment</h2>
            {lastDeploy && <em>
              {get(lastDeploy, 'shortRef')} - {moment(get(lastDeploy, 'finishedAt')).fromNow()}
            </em>}
            {map(lastDeploy.taskResults, (task, i) => <TaskResult key={i} {...task} />)}
          </div>
        }
      </div>
    );
  }
}

ProjectDetails.propTypes = {
  fetchProject: PropTypes.func.isRequired,
  deployProject: PropTypes.func.isRequired,
  project: PropTypes.object.isRequired,
};

const TaskResult = ({ commands, taskType, result }) => {
  let resultKeys = keys(result);
  if (!isEmpty(commands) && !(result && result.hasOwnProperty('ssh'))) {
    resultKeys = commands;
  }

  return (
    <div>
      <h3>{taskType}</h3>
      <code className="overflow-x">
        {map(resultKeys, key => {
          if (!result || !result[key]) return false;
          const { error, stdout, exitCode } = result[key];
          return (
            <div key={taskType + key} className="padding">
              $ <strong>{key}</strong> <span title="Exit code">({exitCode})</span>
              <pre>{stdout || error}</pre>
            </div>
          );
        })}
      </code>
    </div>
  );
};

TaskResult.propTypes = {
  commands: PropTypes.array.isRequired,
  taskType: PropTypes.string.isRequired,
  result: PropTypes.object.isRequired,
};
{'fraction_non_alphanumeric': 0.16270218839200762, 'fraction_numerical': 0.0034253092293054233, 'mean_word_length': 2.597535934291581, 'pattern_counts': {'":': 0, '<': 63, '<?xml version=': 0, '>': 71, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '914745', 'n_tokens_mistral': 1653, 'n_tokens_neox': 1647, 'n_words': 297}