<commit_before>const $ = require('jquery');
const { BagItProfile } = require('../../bagit/bagit_profile');
const { BaseController } = require('./base_controller');
const { Constants } = require('../../core/constants');
const { Context } = require('../../core/context');
const { DartProcess } = require('../../core/dart_process');
const { fork } = require('child_process');
const fs = require('fs');
const { Job } = require('../../core/job');
const { JobRunner } = require('../../workers/job_runner');
const path = require('path');
const Templates = require('../common/templates');
const { UIConstants } = require('../common/ui_constants');
const { UploadTarget } = require('../../core/upload_target');
const { Util } = require('../../core/util');

/**
 * The JobRunController displays the page where users review
 * and run a Job.
 *
 * @param {URLSearchParams} params - The URL search params parsed
 * from the URL used to reach this page. This should contain at
 * least the Job Id.
 *
 * @param {string} params.id - The id of the Job being worked
 * on. Job.id is a UUID string.
 */
class JobRunController extends BaseController {

    constructor(params) {
        super(params, 'Jobs');
        this.model = Job;
        this.job = Job.find(this.params.get('id'));
        this.dartProcess = null;
        this.childProcess = null;
        this.completedUploads = [];
        this.reachedEndOfOutput = false;
    }

    /**
     * Displays a summary of the Job and the "Run Job" button.
     */
    show() {
        let uploadTargets = [];
        for (let op of this.job.uploadOps) {
            let target = UploadTarget.find(op.uploadTargetId);
            if (target) {
                uploadTargets.push(target.name);
            }
        }
        let data = { job: this.job, uploadTargets: uploadTargets }
        return this.containerContent(Templates.jobRun(data));
    }

    /**
     * Runs the Job in a separate process.
     */
    run() {
        // Grey this out while job is running.
        // Run job in separate process, so user can
        // navigate to other screens without disrupting it.
        let tmpFile = Util.tmpFilePath();
        fs.writeFileSync(tmpFile, JSON.stringify(this.job));

        // Need to change npm command outside of dev env.
        let modulePath = path.join(__dirname, '..', '..', 'main.js');
        this.childProcess = fork(
                modulePath,
                ['--job', tmpFile, '--deleteJobFile']
        );

        // TODO: Do we still need this? It's job is to keep
        // track of running jobs for the UI.
        this.dartProcess = new DartProcess(
            this.job.title(),
            tmpFile,
            this.childProcess.pid
        );
        this.dartProcess.save();
        this.initRunningJobDisplay();
        Context.childProcesses[this.dartProcess.id] = this.childProcess;
        return this.noContent();
    }

    initRunningJobDisplay(dartProcess, childProcess) {
        this.showDivs();
        let controller = this;

        this.childProcess.on('message', (data) => {
            controller.renderChildProcOutput(data);
        });

        this.childProcess.on('close', (code) => {
            this.dartProcess.completedAt = new Date().toISOString();
            this.dartProcess.exitCode = code;
            this.renderOutcome(code);
        });
    }

    showDivs() {
        let processDiv = $('#dartProcessContainer');
        let html = Templates.partials['dartProcess']({ item: this.dartProcess });
        processDiv.html(html);
        if (this.job.packageOp && this.job.packageOp.outputPath) {
            $(`#${this.dartProcess.id} div.packageInfo`).show();
            if (this.job.packageOp.packageFormat == 'BagIt') {
                $(`#${this.dartProcess.id} div.validationInfo`).show();
            }
        }
        if (this.job.uploadOps.length > 0) {
            $(`#${this.dartProcess.id} div.uploadInfo`).show();
        }
        processDiv.show();
    }

    renderChildProcOutput(data) {
        switch (data.op) {
        case 'package':
            this.renderPackageInfo(data);
            break;
        case 'validate':
            this.renderValidationInfo(data);
            break;
        case 'upload':
            this.renderUploadInfo(data);
            break;
        default:
            return;
        }
    }

    renderPackageInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.packageInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.packageInfo div.resultIcon`);
        if (data.action == 'fileAdded') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(Context.y18n.__('Added file %s', data.msg));
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.markSuccess(detailDiv, iconDiv, data.msg);
            } else {
                this.markFailed(detailDiv, iconDiv, data.msg);
            }
        } else {
            detailDiv.text(data.msg);
        }
    }

    renderValidationInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.validationInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.validationInfo div.resultIcon`);
        if (data.action == 'checksum') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(Context.y18n.__('Validating %s', data.msg));
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.markSuccess(detailDiv, iconDiv, data.msg);
            } else {
                this.markFailed(detailDiv, iconDiv, data.msg);
            }
        }
    }

    renderUploadInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.uploadInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.uploadInfo div.resultIcon`);
        if (data.action == 'start') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(data.msg);
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.completedUploads.push(data.msg);
                this.markSuccess(detailDiv, iconDiv, this.completedUploads.join("
\n"));
            } else {
                this.markFailed(detailDiv, iconDiv, detailDiv.html());
            }
        }
    }

    // TODO: This has a problem. If we're doing a series of uploads and one
    // fails and it's not the LAST one, this reports a successful outcome.
    // It should report an error.
    renderOutcome(code) {
        // We have to reload this, because the child process updated
        // the job's record in the database.
        this.job = Job.find(this.job.id);
        let detailDiv = $(`#${this.dartProcess.id} div.outcome div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.outcome div.resultIcon`);
        if (code == 0) {
            this.markSuccess(detailDiv, iconDiv, Context.y18n.__('Job completed successfully.'));
        } else {
            this.markFailed(detailDiv, iconDiv, Context.y18n.__(
                'Job did not complete due to errors.'));
        }
    }

    markSuccess(detailDiv, iconDiv, message) {
        detailDiv.html(message);
        iconDiv.html(UIConstants.GREEN_SMILEY);
    }

    markFailed(detailDiv, iconDiv, message) {
        detailDiv.html(message);
        iconDiv.html(UIConstants.RED_ANGRY_FACE);
    }

    postRenderCallback(fnName) {

    }
}

module.exports.JobRunController = JobRunController;
<commit_msg>Display and log job errors. https://trello.com/c/C9BQPB7R<commit_after>const $ = require('jquery');
const { BagItProfile } = require('../../bagit/bagit_profile');
const { BaseController } = require('./base_controller');
const { Constants } = require('../../core/constants');
const { Context } = require('../../core/context');
const { DartProcess } = require('../../core/dart_process');
const { fork } = require('child_process');
const fs = require('fs');
const { Job } = require('../../core/job');
const { JobRunner } = require('../../workers/job_runner');
const path = require('path');
const Templates = require('../common/templates');
const { UIConstants } = require('../common/ui_constants');
const { UploadTarget } = require('../../core/upload_target');
const { Util } = require('../../core/util');

/**
 * The JobRunController displays the page where users review
 * and run a Job.
 *
 * @param {URLSearchParams} params - The URL search params parsed
 * from the URL used to reach this page. This should contain at
 * least the Job Id.
 *
 * @param {string} params.id - The id of the Job being worked
 * on. Job.id is a UUID string.
 */
class JobRunController extends BaseController {

    constructor(params) {
        super(params, 'Jobs');
        this.model = Job;
        this.job = Job.find(this.params.get('id'));
        this.dartProcess = null;
        this.childProcess = null;
        this.completedUploads = [];
        this.reachedEndOfOutput = false;
    }

    /**
     * Displays a summary of the Job and the "Run Job" button.
     */
    show() {
        let uploadTargets = [];
        for (let op of this.job.uploadOps) {
            let target = UploadTarget.find(op.uploadTargetId);
            if (target) {
                uploadTargets.push(target.name);
            }
        }
        let data = { job: this.job, uploadTargets: uploadTargets }
        return this.containerContent(Templates.jobRun(data));
    }

    /**
     * Runs the Job in a separate process.
     */
    run() {
        // Grey this out while job is running.
        // Run job in separate process, so user can
        // navigate to other screens without disrupting it.
        let tmpFile = Util.tmpFilePath();
        fs.writeFileSync(tmpFile, JSON.stringify(this.job));

        // Need to change npm command outside of dev env.
        let modulePath = path.join(__dirname, '..', '..', 'main.js');
        this.childProcess = fork(
                modulePath,
                ['--job', tmpFile, '--deleteJobFile']
        );

        // TODO: Do we still need this? It's job is to keep
        // track of running jobs for the UI.
        this.dartProcess = new DartProcess(
            this.job.title(),
            tmpFile,
            this.childProcess.pid
        );
        this.dartProcess.save();
        this.initRunningJobDisplay();
        Context.childProcesses[this.dartProcess.id] = this.childProcess;
        return this.noContent();
    }

    initRunningJobDisplay(dartProcess, childProcess) {
        this.showDivs();
        let controller = this;

        this.childProcess.on('message', (data) => {
            controller.renderChildProcOutput(data);
        });

        this.childProcess.on('close', (code) => {
            this.dartProcess.completedAt = new Date().toISOString();
            this.dartProcess.exitCode = code;
            this.renderOutcome(code);
        });
    }

    showDivs() {
        let processDiv = $('#dartProcessContainer');
        let html = Templates.partials['dartProcess']({ item: this.dartProcess });
        processDiv.html(html);
        if (this.job.packageOp && this.job.packageOp.outputPath) {
            $(`#${this.dartProcess.id} div.packageInfo`).show();
            if (this.job.packageOp.packageFormat == 'BagIt') {
                $(`#${this.dartProcess.id} div.validationInfo`).show();
            }
        }
        if (this.job.uploadOps.length > 0) {
            $(`#${this.dartProcess.id} div.uploadInfo`).show();
        }
        processDiv.show();
    }

    renderChildProcOutput(data) {
        switch (data.op) {
        case 'package':
            this.renderPackageInfo(data);
            break;
        case 'validate':
            this.renderValidationInfo(data);
            break;
        case 'upload':
            this.renderUploadInfo(data);
            break;
        default:
            return;
        }
    }

    renderPackageInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.packageInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.packageInfo div.resultIcon`);
        if (data.action == 'fileAdded') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(Context.y18n.__('Added file %s', data.msg));
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.markSuccess(detailDiv, iconDiv, data.msg);
            } else {
                this.markFailed(detailDiv, iconDiv, data.msg);
            }
        } else {
            detailDiv.text(data.msg);
        }
    }

    renderValidationInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.validationInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.validationInfo div.resultIcon`);
        if (data.action == 'checksum') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(Context.y18n.__('Validating %s', data.msg));
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.markSuccess(detailDiv, iconDiv, data.msg);
            } else {
                this.markFailed(detailDiv, iconDiv, data.msg);
            }
        }
    }

    renderUploadInfo(data) {
        let detailDiv = $(`#${this.dartProcess.id} div.uploadInfo div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.uploadInfo div.resultIcon`);
        if (data.action == 'start') {
            iconDiv.html(UIConstants.SMALL_BLUE_SPINNER);
            detailDiv.text(data.msg);
        } else if (data.action == 'completed') {
            if (data.status == Constants.OP_SUCCEEDED) {
                this.completedUploads.push(data.msg);
                this.markSuccess(detailDiv, iconDiv, this.completedUploads.join("
\n"));
            } else {
                this.markFailed(detailDiv, iconDiv, detailDiv.html());
            }
        }
    }

    // TODO: This has a problem. If we're doing a series of uploads and one
    // fails and it's not the LAST one, this reports a successful outcome.
    // It should report an error.
    renderOutcome(code) {
        // We have to reload this, because the child process updated
        // the job's record in the database.
        this.job = Job.find(this.job.id);
        let detailDiv = $(`#${this.dartProcess.id} div.outcome div.detail`);
        let iconDiv = $(`#${this.dartProcess.id} div.outcome div.resultIcon`);
        if (code == 0) {
            this.markSuccess(detailDiv, iconDiv, Context.y18n.__('Job completed successfully.'));
        } else {
            let job = Job.find(this.job.id);
            let msg = Context.y18n.__('Job did not complete due to errors.')
            Context.logger.error(msg);
            this.logFailedOps(job);
            msg += `<br/>${job.getRunErrors().join("<br/>")}`
            this.markFailed(detailDiv, iconDiv, msg.replace(/\n/g, '
'));
        }
    }

    logFailedOps(job) {
        if (job.packageOp) {
            Context.logger.error(JSON.stringify(job.packageOp));
        }
        if (job.validationOp) {
            Context.logger.error(JSON.stringify(job.validationOp));
        }
        if (job.uploadOps && job.uploadOps.length > 0) {
            Context.logger.error(JSON.stringify(job.uploadOps));
        }
    }

    markSuccess(detailDiv, iconDiv, message) {
        detailDiv.html(message);
        iconDiv.html(UIConstants.GREEN_SMILEY);
    }

    markFailed(detailDiv, iconDiv, message) {
        detailDiv.html(message);
        iconDiv.html(UIConstants.RED_ANGRY_FACE);
    }

    postRenderCallback(fnName) {

    }
}

module.exports.JobRunController = JobRunController;
<|endoftext|>


<commit_before>'use strict';

var _        = require('lodash');
var d3       = require('d3');

var api      = require('../../data/api');
var coolgray = require('../../colors/coolgray');
var util     = require('../../util/data');

var sort     = require('../../data/transform/sort');

module.exports = {
	replace : true,
	template: require('./stacked.html'),

	paramAttributes: [
		'data-indicators'
	],

	mixins: [
		require('./resize'),
		require('./margin'),
		require('./time-period'),

		require('./hover-tiles'),
		require('./hover-line'),
		require('./labels'),

		require('./xAxis'),
		require('./yGrid')
	],

	data: function () {
		return {
			layers    : [],
			indicators: [],
			region    : null,
			date      : null,
			interval  : null,
			loading   : false,
			error     : false,
			x         : d3.scale.linear(),
			y         : d3.scale.linear(),
		};
	},

	compiled: function () {
		// Convert the comma-separated string of indicator ids into an array of
		// numbers. This allows us to take multiple indicators in a single parameter
		// attribute from the template.
		if (typeof this.indicators === 'string') {
			// Assign to the underlying data object to avoid triggering watchers of
			// the indicators property
			this._data.indicators = this.indicators.split(',').map(Number);
		}
	},

	methods: {

		draw: function () {
			function getX(d) {
				return d.x;
			}

			function getY(d) {
				return d.y0 + d.y;
			}

			function getValues(d) {
				return d.values;
			}

			function defined(d) {
				return util.defined(getY(d));
			}

			if (!this.series) {
				this.$set('series', []);
			}

			var svg = d3.select(this.$el);

			var stack = d3.layout.stack()
				.x(getX)
				.values(getValues);

			var layers  = stack(this.layers);

			var dataset = this.layers.map(getValues);
			var start   = new Date(util.min(dataset, getX));
			var end     = new Date(util.max(dataset, getX));
			var lower   = Math.min(0, util.min(dataset, function (d) { return d.y0; }));
			var upper   = util.max(dataset, getY) * 1.1;

			var x = this.x
				.domain([start, end])
				.range([0, this.contentWidth]);

			var y = this.y
				.domain([lower, upper])
				.range([this.contentHeight, 0]);

			var color = d3.scale.ordinal()
				.domain([0, this.series.length])
				.range(coolgray);

			var area = d3.svg.area()
				.defined(defined)
				.x(function (d) { return x(getX(d)); })
				.y0(function (d) { return y(d.y0); })
				.y1(function (d) { return y(getY(d)); });

			var paths = svg.select('.data').selectAll('.layer')
				.data(layers);

			paths.enter().append('path')
				.attr('class', 'layer');

			paths.attr('d', function (d) { return area(getValues(d)); })
				.style('fill', function (d, i) { return color(i); });

			paths.exit().remove();

			this._callHook('drawn');
		},

		load: _.throttle(function () {
			// Make sure we have all the required properties to build a query
			if (!this.indicators || !this.region) {
				this.loading = false;
				return;
			}

			this.loading = true;

			var self = this;

			var q = _.extend({
				indicator__in: this.indicators,
				region__in   : [this.region]
			}, this.interval);

			api.datapoints(q)
				.then(function (data) {
					return data.objects;
				})
				.then(sort(function (d) { return d.campaign.start_date; }))
				.then(function (data) {
					var layers = {};

					for (var i = 0, l = data.length; i < l; i++) {
						var d = data[i];
						var values = _.indexBy(d.indicators, 'indicator');

						for (var j = self.indicators.length - 1; j >= 0; j--) {
							var ind = self.indicators[j];

							if (!layers.hasOwnProperty(ind)) {
								layers[ind] = [];
							}

							layers[ind].push({
								x: d.campaign.start_date.getTime(),
								y: values[ind].value
							});
						}
					}

					return _.transform(layers, function (result, values, indicator) {
						result.push({
							'name'  : indicator,
							'values': values
						});
					}, []);
				})
				.done(function (data) {
					self.layers  = data;

					self.$emit('invalidate-display');
				}, this.onError);
		}, 300, { trailing: true, leading: false }),

		onError: function () {
			this.error = true;
		}

	},

	events: {
		'data-load'         : 'load',
		'invalidate-size'   : 'draw',
		'invalidate-display': 'draw'
	}
};
<commit_msg>Set the sort order to inside-out<commit_after>'use strict';

var _        = require('lodash');
var d3       = require('d3');

var api      = require('../../data/api');
var coolgray = require('../../colors/coolgray');
var util     = require('../../util/data');

var sort     = require('../../data/transform/sort');

module.exports = {
	replace : true,
	template: require('./stacked.html'),

	paramAttributes: [
		'data-indicators'
	],

	mixins: [
		require('./resize'),
		require('./margin'),
		require('./time-period'),

		require('./hover-tiles'),
		require('./hover-line'),
		require('./labels'),

		require('./xAxis'),
		require('./yGrid')
	],

	data: function () {
		return {
			layers    : [],
			indicators: [],
			region    : null,
			date      : null,
			interval  : null,
			loading   : false,
			error     : false,
			x         : d3.scale.linear(),
			y         : d3.scale.linear(),
		};
	},

	compiled: function () {
		// Convert the comma-separated string of indicator ids into an array of
		// numbers. This allows us to take multiple indicators in a single parameter
		// attribute from the template.
		if (typeof this.indicators === 'string') {
			// Assign to the underlying data object to avoid triggering watchers of
			// the indicators property
			this._data.indicators = this.indicators.split(',').map(Number);
		}
	},

	methods: {

		draw: function () {
			function getX(d) {
				return d.x;
			}

			function getY(d) {
				return d.y0 + d.y;
			}

			function getValues(d) {
				return d.values;
			}

			function defined(d) {
				return util.defined(getY(d));
			}

			if (!this.series) {
				this.$set('series', []);
			}

			var svg = d3.select(this.$el);

			var stack = d3.layout.stack()
				.x(getX)
				.values(getValues)
				.order('inside-out');

			var layers  = stack(this.layers);

			var dataset = this.layers.map(getValues);
			var start   = new Date(util.min(dataset, getX));
			var end     = new Date(util.max(dataset, getX));
			var lower   = Math.min(0, util.min(dataset, function (d) { return d.y0; }));
			var upper   = util.max(dataset, getY) * 1.1;

			var x = this.x
				.domain([start, end])
				.range([0, this.contentWidth]);

			var y = this.y
				.domain([lower, upper])
				.range([this.contentHeight, 0]);

			var color = d3.scale.ordinal()
				.domain([0, this.series.length])
				.range(coolgray);

			var area = d3.svg.area()
				.defined(defined)
				.x(function (d) { return x(getX(d)); })
				.y0(function (d) { return y(d.y0); })
				.y1(function (d) { return y(getY(d)); });

			var paths = svg.select('.data').selectAll('.layer')
				.data(layers);

			paths.enter().append('path')
				.attr('class', 'layer');

			paths.attr('d', function (d) { return area(getValues(d)); })
				.style('fill', function (d, i) { return color(i); });

			paths.exit().remove();

			this._callHook('drawn');
		},

		load: _.throttle(function () {
			// Make sure we have all the required properties to build a query
			if (!this.indicators || !this.region) {
				this.loading = false;
				return;
			}

			this.loading = true;

			var self = this;

			var q = _.extend({
				indicator__in: this.indicators,
				region__in   : [this.region]
			}, this.interval);

			api.datapoints(q)
				.then(function (data) {
					return data.objects;
				})
				.then(sort(function (d) { return d.campaign.start_date; }))
				.then(function (data) {
					var layers = {};

					for (var i = 0, l = data.length; i < l; i++) {
						var d = data[i];
						var values = _.indexBy(d.indicators, 'indicator');

						for (var j = self.indicators.length - 1; j >= 0; j--) {
							var ind = self.indicators[j];

							if (!layers.hasOwnProperty(ind)) {
								layers[ind] = [];
							}

							layers[ind].push({
								x: d.campaign.start_date.getTime(),
								y: values[ind].value
							});
						}
					}

					return _.transform(layers, function (result, values, indicator) {
						result.push({
							'name'  : indicator,
							'values': values
						});
					}, []);
				})
				.done(function (data) {
					self.layers  = data;

					self.$emit('invalidate-display');
				}, this.onError);
		}, 300, { trailing: true, leading: false }),

		onError: function () {
			this.error = true;
		}

	},

	events: {
		'data-load'         : 'load',
		'invalidate-size'   : 'draw',
		'invalidate-display': 'draw'
	}
};
<|endoftext|>


<commit_before>import { get } from 'lodash';

import * as actions from 'src/actions';
import { showWarningToast } from './toast';

const errorActionTypes = [
  actions.fetchMetadata.ERROR,
  actions.fetchStatistics.ERROR,
  actions.queryEntities.ERROR,
  actions.queryCollections.ERROR,
  actions.fetchEntity.ERROR,
  actions.fetchEntityReferences.ERROR,
  actions.fetchEntityTags.ERROR,
  actions.fetchDocument.ERROR,
  actions.fetchCollection.ERROR,
  actions.queryDocumentRecords.ERROR,
];

// Middleware that after handling any action normally, shows a toast if the
// action was reporting an error.
const errorToastMiddleware = store => next => action => {
  const newState = next(action);

  if (errorActionTypes.includes(action.type)) {
    const defaultDescription = 'No clue how that happened.';
    const description = get(action, 'payload.error.message', defaultDescription);
    const message = `${action.type}: ${description}`;
    console.error(message);
    showWarningToast(message);
  }

  return newState;
};

export default errorToastMiddleware;
<commit_msg>Don’t show action type in error messages.<commit_after>import { get } from 'lodash';

import * as actions from 'src/actions';
import { showWarningToast } from './toast';

const errorActionTypes = [
  actions.fetchMetadata.ERROR,
  actions.fetchStatistics.ERROR,
  actions.queryEntities.ERROR,
  actions.queryCollections.ERROR,
  actions.fetchEntity.ERROR,
  actions.fetchEntityReferences.ERROR,
  actions.fetchEntityTags.ERROR,
  actions.fetchDocument.ERROR,
  actions.fetchCollection.ERROR,
  actions.queryDocumentRecords.ERROR,
];

// Middleware that after handling any action normally, shows a toast if the
// action was reporting an error.
const errorToastMiddleware = store => next => action => {
  const newState = next(action);

  if (errorActionTypes.includes(action.type)) {
    const defaultDescription = 'No clue how that happened.';
    const description = get(action, 'payload.error.message', defaultDescription);
    showWarningToast(description);
    console.error(action.type, description);
  }

  return newState;
};

export default errorToastMiddleware;
<|endoftext|>


<commit_before>/*
 * LICENCE : CloudUnit is available under the Affero Gnu Public License GPL V3 : https://www.gnu.org/licenses/agpl-3.0.html
 *     but CloudUnit is licensed too under a standard commercial license.
 *     Please contact our sales team if you would like to discuss the specifics of our Enterprise license.
 *     If you are not sure whether the GPL is right for you,
 *     you can always test our software under the GPL and inspect the source code before you contact us
 *     about purchasing a commercial license.
 *
 *     LEGAL TERMS : "CloudUnit" is a registered trademark of Treeptik and can't be used to endorse
 *     or promote products derived from this project without prior written permission from Treeptik.
 *     Products or services derived from this software may not be called "CloudUnit"
 *     nor may "Treeptik" or similar confusing terms appear in their names without prior written permission.
 *     For any questions, contact us : contact@treeptik.fr
 */

(function () {
  'use strict';

  angular
    .module ( 'webuiApp.editApplication' )
    .directive ( 'editAppMonitoringApplication', MonitoringApp );

  function MonitoringApp () {
    return {
      restrict: 'E',
      templateUrl: 'scripts/components/editApplication/monitoring/application/editApplication.monitoring.application.html',
      scope: {
        app: '='
      },
      controller: [
        '$scope',
        'ApplicationService',
        '$stateParams',
        'MonitoringService',
        '$interval',
        MonitoringAppCtrl
      ],
      controllerAs: 'monitoringApp',
      bindToController: true
    };
  }

  function MonitoringAppCtrl ( $scope, ApplicationService, $stateParams, MonitoringService, $interval ) {

    // ------------------------------------------------------------------------
    // MONITORING
    // ------------------------------------------------------------------------

    var vm = this;
    vm.cleanFirstValue = true;
    vm.queueName = '';
    vm.queueStats = {};
    vm.selectedQueueStats = {};
    vm.displayGraph = [];
    vm.queueNameTab = [];
    vm.timer = {};
    var NumberOfDataByGraph = 120;
    vm.loadStats = loadStats;
    vm.deleteGraph = deleteGraph;
    vm.chooseQueue = chooseQueue;

    var lastQueueNameSelected = '';
    $scope.$on ( '$destroy', function () {
      $interval.cancel(vm.timer);
      vm.timer = null;
    } );

    init();

    function init() {
      setTimeout(function() {
        MonitoringService.getUrlMetrics(vm.app.servers[0].image.prefixEnv)
          .then(function success(data) {
            angular.forEach(data, function(predefinedGraph, index) {
                referenceQueue(predefinedGraph.url);

                vm.displayGraph.push({
                  data: [],
                  title: predefinedGraph.url + " " + predefinedGraph.name,
                  id: predefinedGraph.suffix,
                  location: predefinedGraph.url,
                  description: predefinedGraph.url,
                  x_accessor:'date',
                  y_accessor:'value',
                  area: false,
                  interpolate: 'basic',
                });

            });
            pollStats();
          })
          .catch(function(err){
            console.log(err);
          });
      }, 0);

      vm.timer = $interval(pollStats, 5000);
    }

    function pollStats() {
          angular.forEach(vm.queueNameTab, function(queueName, key) {
            MonitoringService.chooseQueue(vm.app.location, queueName)
            .then(function(response) {
              angular.forEach(vm.displayGraph, function(value, key) {

                if(value.location == lastQueueNameSelected && vm.cleanFirstValue) {
                  vm.selectedQueueStats[value.id] = true;
                  vm.displayGraph[key].data.shift();
                }
                if(vm.displayGraph[key].location == queueName) {

                  vm.displayGraph[key].data.push(
                    {
                      "date":new Date(response.timestamp*1000),
                      "value":response.value[value.id]
                    }
                  );
                  if(vm.displayGraph[key].data.length >= NumberOfDataByGraph) {
                    vm.displayGraph[key].data.splice(0, 1);
                  }
                }
              });
              vm.cleanFirstValue = false;
            })
          });
        }

    function referenceQueue(queueName) {
      if(!vm.queueNameTab.includes(queueName)) {
         vm.queueNameTab.push(queueName);
      }
      lastQueueNameSelected = queueName;
    }

    function chooseQueue(queueName) {
      vm.cleanFirstValue = true;
      MonitoringService.chooseQueue(vm.app.location, queueName)
        .then(success)
        .catch(error);

      function success(response) {
        vm.queueStats = response.value;

        // clean
        referenceQueue(vm.queueName);
        vm.errorMsg = '';
        vm.queueName = '';
        refreshSelectedQueueStats();
      }

      function error(response) {
        vm.errorMsg = response.data.message;
        return vm.errorMsg;
      }
    }

    function cleanAll() {
      vm.cleanFirstValue = true;
      angular.forEach(vm.displayGraph , function(value, index) {
        //delete all element already selected for the current queue
        vm.displayGraph[index].data = [];
      });

      vm.displayGraph = vm.displayGraph.filter(
        function(x) {
          return lastQueueNameSelected != x.location;
        }
      );
    }

    function loadStats(selectedQueueStats) {

      cleanAll();

      setTimeout(function() {
        angular.forEach(selectedQueueStats, function(isQueueSelected, key) {
          if(isQueueSelected) {
            vm.displayGraph.push({
              data: [],
              title: lastQueueNameSelected + " " + key,
              id: key,
              location: lastQueueNameSelected,
              description: lastQueueNameSelected,
              x_accessor:'date',
              y_accessor:'value',
              area: false,
              interpolate: 'basic',
            });
          }
        });
      }, 0);

    }

   function refreshSelectedQueueStats() {
      vm.selectedQueueStats = {};
      var res = vm.displayGraph.filter(
        function(x) {
          return lastQueueNameSelected == x.location;
        }
      );

      angular.forEach(res, function(graph, index) {
        console.log(graph.id);
        vm.selectedQueueStats[graph.id] = true;
      });
   }

   function deleteGraph(queueStatsName, queueLocation) {
     angular.forEach(vm.displayGraph, function(graph, index) {
       if(graph.id == queueStatsName && graph.location == queueLocation) {
        vm.displayGraph.splice(index, 1);
       }
     });
     refreshSelectedQueueStats();
   }

  }
}) ();

<commit_msg>ApplicationMonitoring Directive to Component<commit_after>/*
 * LICENCE : CloudUnit is available under the Affero Gnu Public License GPL V3 : https://www.gnu.org/licenses/agpl-3.0.html
 *     but CloudUnit is licensed too under a standard commercial license.
 *     Please contact our sales team if you would like to discuss the specifics of our Enterprise license.
 *     If you are not sure whether the GPL is right for you,
 *     you can always test our software under the GPL and inspect the source code before you contact us
 *     about purchasing a commercial license.
 *
 *     LEGAL TERMS : "CloudUnit" is a registered trademark of Treeptik and can't be used to endorse
 *     or promote products derived from this project without prior written permission from Treeptik.
 *     Products or services derived from this software may not be called "CloudUnit"
 *     nor may "Treeptik" or similar confusing terms appear in their names without prior written permission.
 *     For any questions, contact us : contact@treeptik.fr
 */

(function () {
  'use strict';

  angular
    .module ( 'webuiApp.editApplication' )
    .component ( 'editAppMonitoringApplication', MonitoringApp() );

  function MonitoringApp () {
    return {
      templateUrl: 'scripts/components/editApplication/monitoring/application/editApplication.monitoring.application.html',
      bindings: {
        app: '='
      },
      controller: [
        '$scope',
        'ApplicationService',
        '$stateParams',
        'MonitoringService',
        '$interval',
        MonitoringAppCtrl
      ],
      controllerAs: 'monitoringApp',
    };
  }

  function MonitoringAppCtrl ( $scope, ApplicationService, $stateParams, MonitoringService, $interval ) {

    // ------------------------------------------------------------------------
    // MONITORING
    // ------------------------------------------------------------------------

    var vm = this;
    vm.cleanFirstValue = true;
    vm.queueName = '';
    vm.queueStats = {};
    vm.selectedQueueStats = {};
    vm.displayGraph = [];
    vm.queueNameTab = [];
    vm.timer = {};
    var NumberOfDataByGraph = 120;
    vm.loadStats = loadStats;
    vm.deleteGraph = deleteGraph;
    vm.chooseQueue = chooseQueue;

    var lastQueueNameSelected = '';

    vm.$onDestroy = function () {
      $interval.cancel(vm.timer);
      vm.timer = null;
    };

    vm.$onInit = function() {
      setTimeout(function() {
        MonitoringService.getUrlMetrics(vm.app.servers[0].image.prefixEnv)
          .then(function success(data) {
            angular.forEach(data, function(predefinedGraph, index) {
                referenceQueue(predefinedGraph.url);

                vm.displayGraph.push({
                  data: [],
                  title: predefinedGraph.url + " " + predefinedGraph.name,
                  id: predefinedGraph.suffix,
                  location: predefinedGraph.url,
                  description: predefinedGraph.url,
                  x_accessor:'date',
                  y_accessor:'value',
                  area: false,
                  interpolate: 'basic',
                });

            });
            pollStats();
          })
          .catch(function(err){
            console.log(err);
          });
      }, 0);

      vm.timer = $interval(pollStats, 5000);
    }

    function pollStats() {
          angular.forEach(vm.queueNameTab, function(queueName, key) {
            MonitoringService.chooseQueue(vm.app.location, queueName)
            .then(function(response) {
              angular.forEach(vm.displayGraph, function(value, key) {

                if(value.location == lastQueueNameSelected && vm.cleanFirstValue) {
                  vm.selectedQueueStats[value.id] = true;
                  vm.displayGraph[key].data.shift();
                }
                if(vm.displayGraph[key].location == queueName) {

                  vm.displayGraph[key].data.push(
                    {
                      "date":new Date(response.timestamp*1000),
                      "value":response.value[value.id]
                    }
                  );
                  if(vm.displayGraph[key].data.length >= NumberOfDataByGraph) {
                    vm.displayGraph[key].data.splice(0, 1);
                  }
                }
              });
              vm.cleanFirstValue = false;
            })
          });
        }

    function referenceQueue(queueName) {
      if(!vm.queueNameTab.includes(queueName)) {
         vm.queueNameTab.push(queueName);
      }
      lastQueueNameSelected = queueName;
    }

    function chooseQueue(queueName) {
      vm.cleanFirstValue = true;
      MonitoringService.chooseQueue(vm.app.location, queueName)
        .then(success)
        .catch(error);

      function success(response) {
        vm.queueStats = response.value;

        // clean
        referenceQueue(vm.queueName);
        vm.errorMsg = '';
        vm.queueName = '';
        refreshSelectedQueueStats();
      }

      function error(response) {
        vm.errorMsg = response.data.message;
        return vm.errorMsg;
      }
    }

    function cleanAll() {
      vm.cleanFirstValue = true;
      angular.forEach(vm.displayGraph , function(value, index) {
        //delete all element already selected for the current queue
        vm.displayGraph[index].data = [];
      });

      vm.displayGraph = vm.displayGraph.filter(
        function(x) {
          return lastQueueNameSelected != x.location;
        }
      );
    }

    function loadStats(selectedQueueStats) {

      cleanAll();

      setTimeout(function() {
        angular.forEach(selectedQueueStats, function(isQueueSelected, key) {
          if(isQueueSelected) {
            vm.displayGraph.push({
              data: [],
              title: lastQueueNameSelected + " " + key,
              id: key,
              location: lastQueueNameSelected,
              description: lastQueueNameSelected,
              x_accessor:'date',
              y_accessor:'value',
              area: false,
              interpolate: 'basic',
            });
          }
        });
      }, 0);

    }

   function refreshSelectedQueueStats() {
      vm.selectedQueueStats = {};
      var res = vm.displayGraph.filter(
        function(x) {
          return lastQueueNameSelected == x.location;
        }
      );

      angular.forEach(res, function(graph, index) {
        console.log(graph.id);
        vm.selectedQueueStats[graph.id] = true;
      });
   }

   function deleteGraph(queueStatsName, queueLocation) {
     angular.forEach(vm.displayGraph, function(graph, index) {
       if(graph.id == queueStatsName && graph.location == queueLocation) {
        vm.displayGraph.splice(index, 1);
       }
     });
     refreshSelectedQueueStats();
   }

  }
}) ();

<|endoftext|>


<commit_before>define(["app/ecs/component"], function(Component) {
    "use strict";

    var Direction = Component.extend({
        name: "direction",
        init: function(x, y) {
            this.x = x;
            this.y = y;
        },
        length: function() {
            return Math.sqrt(this.x, this.y);
        },
        normalise: function() {
            var length = this.length();
            if (length > 0) {
                this.x /= length;
                this.y /= length;
            }
        }
    });

    return Direction;
});<commit_msg>Fix normalise for direction component<commit_after>define(["app/ecs/component"], function(Component) {
    "use strict";

    var Direction = Component.extend({
        name: "direction",
        init: function(x, y) {
            this.x = x;
            this.y = y;
        },
        length: function() {
            return Math.sqrt(this.x * this.x + this.y * this.y);
        },
        normalise: function() {
            var length = this.length();
            if (length > 0) {
                this.x /= length;
                this.y /= length;
            }
        }
    });

    return Direction;
});<|endoftext|>


<commit_before>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {CollapseReplies} from '../../components/CollapseReplies/CollapseReplies'
import {
  DELETE_DISCUSSION_ENTRY,
  UPDATE_DISCUSSION_ENTRY_PARTICIPANT
} from '../../../graphql/Mutations'
import {DeletedPostMessage} from '../../components/DeletedPostMessage/DeletedPostMessage'
import {DISCUSSION_SUBENTRIES_QUERY} from '../../../graphql/Queries'
import {DiscussionEntry} from '../../../graphql/DiscussionEntry'
import {Flex} from '@instructure/ui-flex'
import I18n from 'i18n!discussion_topics_post'
import LoadingIndicator from '@canvas/loading-indicator'
import {PostMessage} from '../../components/PostMessage/PostMessage'
import {PER_PAGE} from '../../utils/constants'
import PropTypes from 'prop-types'
import React, {useContext, useState} from 'react'
import {ThreadActions} from '../../components/ThreadActions/ThreadActions'
import {ThreadingToolbar} from '../../components/ThreadingToolbar/ThreadingToolbar'
import {useMutation, useQuery} from 'react-apollo'
import {View} from '@instructure/ui-view'

export const mockThreads = {
  id: '432',
  author: {
    name: 'Jeffrey Johnson',
    avatarUrl: 'someURL'
  },
  createdAt: '2021-02-08T13:36:05-07:00',
  message:
    '
This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends.
',
  read: true,
  lastReply: null,
  rootEntryParticipantCounts: {
    unreadCount: 0,
    repliesCount: 0
  },
  subentriesCount: 0
}

export const DiscussionThreadContainer = props => {
  const {setOnFailure, setOnSuccess} = useContext(AlertManagerContext)
  const [expandReplies, setExpandReplies] = useState(false)

  const [deleteDiscussionEntry] = useMutation(DELETE_DISCUSSION_ENTRY, {
    onCompleted: data => {
      if (!data.deleteDiscussionEntry.errors) {
        setOnSuccess(I18n.t('The entry was successfully deleted'))
      } else {
        setOnFailure(I18n.t('There was an unexpected error while deleting the entry'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error while deleting the entry'))
    }
  })
  const [updateDiscussionEntryParticipant] = useMutation(UPDATE_DISCUSSION_ENTRY_PARTICIPANT, {
    onCompleted: data => {
      if (!data || !data.updateDiscussionEntryParticipant) {
        return null
      }
      setOnSuccess(I18n.t('The entry was successfully updated.'))
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the entry.'))
    }
  })
  const toggleRating = () => {
    updateDiscussionEntryParticipant({
      variables: {
        discussionEntryId: props._id,
        rating: props.rating ? 'not_liked' : 'liked'
      }
    })
  }

  const toggleUnread = () => {
    updateDiscussionEntryParticipant({
      variables: {
        discussionEntryId: props._id,
        read: !props.read
      }
    })
  }

  const marginDepth = 4 * props.depth

  const threadActions = []
  if (!props.deleted) {
    threadActions.push(<ThreadingToolbar.Reply key={`reply-${props.id}`} onReply={() => {}} />)
    threadActions.push(
      <ThreadingToolbar.Like
        key={`like-${props.id}`}
        onClick={toggleRating}
        isLiked={props.rating}
        likeCount={props.ratingSum || 0}
      />
    )
  }

  const createdAt = Date.parse(props.createdAt)

  if (props.depth === 0 && props.lastReply) {
    threadActions.push(
      <ThreadingToolbar.Expansion
        key={`expand-${props.id}`}
        expandText={I18n.t('%{replies} replies, %{unread} unread', {
          replies: props.rootEntryParticipantCounts?.repliesCount,
          unread: props.rootEntryParticipantCounts?.unreadCount
        })}
        onClick={() => setExpandReplies(!expandReplies)}
      />
    )
  }

  const onDelete = () => {
    if (window.confirm(I18n.t('Are you sure you want to delete this entry?'))) {
      deleteDiscussionEntry({
        variables: {
          id: props._id
        }
      })
    }
  }

  const renderPostMessage = () => {
    if (props.deleted) {
      const name = props.editor ? props.editor.name : props.author.name
      return (
        <DeletedPostMessage deleterName={name} timingDisplay={createdAt.toDateString()}>
          <ThreadingToolbar>{threadActions}</ThreadingToolbar>
        </DeletedPostMessage>
      )
    } else {
      return (
        <PostMessage
          authorName={props.author.name}
          avatarUrl={props.author.avatarUrl}
          timingDisplay={createdAt.toDateString()}
          message={props.message}
          isUnread={!props.read}
        >
          <ThreadingToolbar>{threadActions}</ThreadingToolbar>
        </PostMessage>
      )
    }
  }

  return (
    <>

        <Flex>
          <Flex.Item shouldShrink shouldGrow>
            {renderPostMessage()}
          </Flex.Item>
          {!props.deleted && (
            <Flex.Item align="stretch">
              <ThreadActions
                id={props.id}
                isUnread={!props.read}
                onToggleUnread={toggleUnread}
                onMarkAllAsUnread={() => {}}
                onDelete={props.permissions.delete ? onDelete : null}
              />
            </Flex.Item>
          )}
        </Flex>

      {(expandReplies || props.depth > 0) && props.subentriesCount > 0 && (
        <DiscussionSubentries discussionEntryId={props._id} depth={props.depth + 1} />
      )}
      {expandReplies && props.depth === 0 && props.lastReply && (
        <div
          style={{marginLeft: '4rem'}}
          width="100%"
          key={`discussion-thread-collapse-${props.id}`}
        >
          <View
            background="primary"
            borderWidth="none none small none"
            padding="none none small none"
            display="block"
            width="100%"
            margin="none none medium none"
          >
            <CollapseReplies onClick={() => setExpandReplies(false)} />
          </View>

      )}
    </>
  )
}

DiscussionThreadContainer.propTypes = {
  ...DiscussionEntry.shape,
  depth: PropTypes.number
}

DiscussionThreadContainer.defaultProps = {
  depth: 0
}

export default DiscussionThreadContainer

const DiscussionSubentries = props => {
  const {setOnFailure} = useContext(AlertManagerContext)

  const subentries = useQuery(DISCUSSION_SUBENTRIES_QUERY, {
    variables: {
      discussionEntryID: props.discussionEntryId,
      perPage: PER_PAGE
    }
  })

  if (subentries.error) {
    setOnFailure(I18n.t('Error loading replies'))
    return null
  }

  if (subentries.loading) {
    return <LoadingIndicator />
  }

  return subentries.data.legacyNode.discussionSubentriesConnection.nodes.map(entry => (
    <DiscussionThreadContainer
      key={`discussion-thread-${entry.id}`}
      depth={props.depth}
      {...entry}
    />
  ))
}

DiscussionSubentries.propTypes = {
  discussionEntryId: PropTypes.string,
  depth: PropTypes.number
}
<commit_msg>provide isExpanded required prop<commit_after>/*
 * Copyright (C) 2021 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {CollapseReplies} from '../../components/CollapseReplies/CollapseReplies'
import {
  DELETE_DISCUSSION_ENTRY,
  UPDATE_DISCUSSION_ENTRY_PARTICIPANT
} from '../../../graphql/Mutations'
import {DeletedPostMessage} from '../../components/DeletedPostMessage/DeletedPostMessage'
import {DISCUSSION_SUBENTRIES_QUERY} from '../../../graphql/Queries'
import {DiscussionEntry} from '../../../graphql/DiscussionEntry'
import {Flex} from '@instructure/ui-flex'
import I18n from 'i18n!discussion_topics_post'
import LoadingIndicator from '@canvas/loading-indicator'
import {PostMessage} from '../../components/PostMessage/PostMessage'
import {PER_PAGE} from '../../utils/constants'
import PropTypes from 'prop-types'
import React, {useContext, useState} from 'react'
import {ThreadActions} from '../../components/ThreadActions/ThreadActions'
import {ThreadingToolbar} from '../../components/ThreadingToolbar/ThreadingToolbar'
import {useMutation, useQuery} from 'react-apollo'
import {View} from '@instructure/ui-view'

export const mockThreads = {
  id: '432',
  author: {
    name: 'Jeffrey Johnson',
    avatarUrl: 'someURL'
  },
  createdAt: '2021-02-08T13:36:05-07:00',
  message:
    '
This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends. This is the post that never ends. It goes on and on my friends.
',
  read: true,
  lastReply: null,
  rootEntryParticipantCounts: {
    unreadCount: 0,
    repliesCount: 0
  },
  subentriesCount: 0
}

export const DiscussionThreadContainer = props => {
  const {setOnFailure, setOnSuccess} = useContext(AlertManagerContext)
  const [expandReplies, setExpandReplies] = useState(false)

  const [deleteDiscussionEntry] = useMutation(DELETE_DISCUSSION_ENTRY, {
    onCompleted: data => {
      if (!data.deleteDiscussionEntry.errors) {
        setOnSuccess(I18n.t('The entry was successfully deleted'))
      } else {
        setOnFailure(I18n.t('There was an unexpected error while deleting the entry'))
      }
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error while deleting the entry'))
    }
  })
  const [updateDiscussionEntryParticipant] = useMutation(UPDATE_DISCUSSION_ENTRY_PARTICIPANT, {
    onCompleted: data => {
      if (!data || !data.updateDiscussionEntryParticipant) {
        return null
      }
      setOnSuccess(I18n.t('The entry was successfully updated.'))
    },
    onError: () => {
      setOnFailure(I18n.t('There was an unexpected error updating the entry.'))
    }
  })
  const toggleRating = () => {
    updateDiscussionEntryParticipant({
      variables: {
        discussionEntryId: props._id,
        rating: props.rating ? 'not_liked' : 'liked'
      }
    })
  }

  const toggleUnread = () => {
    updateDiscussionEntryParticipant({
      variables: {
        discussionEntryId: props._id,
        read: !props.read
      }
    })
  }

  const marginDepth = 4 * props.depth

  const threadActions = []
  if (!props.deleted) {
    threadActions.push(<ThreadingToolbar.Reply key={`reply-${props.id}`} onReply={() => {}} />)
    threadActions.push(
      <ThreadingToolbar.Like
        key={`like-${props.id}`}
        onClick={toggleRating}
        isLiked={props.rating}
        likeCount={props.ratingSum || 0}
      />
    )
  }

  const createdAt = Date.parse(props.createdAt)

  if (props.depth === 0 && props.lastReply) {
    threadActions.push(
      <ThreadingToolbar.Expansion
        key={`expand-${props.id}`}
        expandText={I18n.t('%{replies} replies, %{unread} unread', {
          replies: props.rootEntryParticipantCounts?.repliesCount,
          unread: props.rootEntryParticipantCounts?.unreadCount
        })}
        onClick={() => setExpandReplies(!expandReplies)}
        isExpanded={expandReplies}
      />
    )
  }

  const onDelete = () => {
    if (window.confirm(I18n.t('Are you sure you want to delete this entry?'))) {
      deleteDiscussionEntry({
        variables: {
          id: props._id
        }
      })
    }
  }

  const renderPostMessage = () => {
    if (props.deleted) {
      const name = props.editor ? props.editor.name : props.author.name
      return (
        <DeletedPostMessage deleterName={name} timingDisplay={createdAt.toDateString()}>
          <ThreadingToolbar>{threadActions}</ThreadingToolbar>
        </DeletedPostMessage>
      )
    } else {
      return (
        <PostMessage
          authorName={props.author.name}
          avatarUrl={props.author.avatarUrl}
          timingDisplay={createdAt.toDateString()}
          message={props.message}
          isUnread={!props.read}
        >
          <ThreadingToolbar>{threadActions}</ThreadingToolbar>
        </PostMessage>
      )
    }
  }

  return (
    <>

        <Flex>
          <Flex.Item shouldShrink shouldGrow>
            {renderPostMessage()}
          </Flex.Item>
          {!props.deleted && (
            <Flex.Item align="stretch">
              <ThreadActions
                id={props.id}
                isUnread={!props.read}
                onToggleUnread={toggleUnread}
                onMarkAllAsUnread={() => {}}
                onDelete={props.permissions.delete ? onDelete : null}
              />
            </Flex.Item>
          )}
        </Flex>

      {(expandReplies || props.depth > 0) && props.subentriesCount > 0 && (
        <DiscussionSubentries discussionEntryId={props._id} depth={props.depth + 1} />
      )}
      {expandReplies && props.depth === 0 && props.lastReply && (
        <div
          style={{marginLeft: '4rem'}}
          width="100%"
          key={`discussion-thread-collapse-${props.id}`}
        >
          <View
            background="primary"
            borderWidth="none none small none"
            padding="none none small none"
            display="block"
            width="100%"
            margin="none none medium none"
          >
            <CollapseReplies onClick={() => setExpandReplies(false)} />
          </View>

      )}
    </>
  )
}

DiscussionThreadContainer.propTypes = {
  ...DiscussionEntry.shape,
  depth: PropTypes.number
}

DiscussionThreadContainer.defaultProps = {
  depth: 0
}

export default DiscussionThreadContainer

const DiscussionSubentries = props => {
  const {setOnFailure} = useContext(AlertManagerContext)

  const subentries = useQuery(DISCUSSION_SUBENTRIES_QUERY, {
    variables: {
      discussionEntryID: props.discussionEntryId,
      perPage: PER_PAGE
    }
  })

  if (subentries.error) {
    setOnFailure(I18n.t('Error loading replies'))
    return null
  }

  if (subentries.loading) {
    return <LoadingIndicator />
  }

  return subentries.data.legacyNode.discussionSubentriesConnection.nodes.map(entry => (
    <DiscussionThreadContainer
      key={`discussion-thread-${entry.id}`}
      depth={props.depth}
      {...entry}
    />
  ))
}

DiscussionSubentries.propTypes = {
  discussionEntryId: PropTypes.string,
  depth: PropTypes.number
}
<|endoftext|>


<commit_before>angular.module('panzoomwidget', [])
.directive('panzoomwidget', function() {
	return {
		restrict: 'E',
		transclude: false,
		scope: {
			config: '=',
			model: '='
		},
		controller: ['$scope', '$element', function($scope, $element) {
//			console.log('init panzoomwidget.js');

			var zoomSliderWidget = $element.find('.zoom-slider-widget');

			$scope.getZoomLevels = function() {
				var zoomLevels = [];
				for (var i = 0; i < $scope.config.zoomLevels; i++) {
					zoomLevels.push(i);
				}
				return zoomLevels;
			};

			$scope.widgetConfig = {
				zoomLevelHeight : 10
			};

			$scope.zoomIn = function() {
				$scope.model.zoomIn();
			};

			$scope.zoomOut = function() {
				$scope.model.zoomOut();
			};

			// $watch is not fast enough so we set up our own polling
			setInterval(function() {
				zoomSliderWidget.css('top', (($scope.config.zoomLevels - $scope.model.zoomLevel - 1) * $scope.widgetConfig.zoomLevelHeight) + 'px');
			}, 25);
		}],
		template:
			'
' +
				'
+
' +
				'
' +
					'

' +
					'

' +
				'
' +
				'
-
' +
			'
',
		replace: true
	};
});
<commit_msg>widget now supports zooming to specific level by clicking at the slider bar<commit_after>angular.module('panzoomwidget', [])
.directive('panzoomwidget', function() {
	return {
		restrict: 'E',
		transclude: false,
		scope: {
			config: '=',
			model: '='
		},
		controller: ['$scope', '$element', function($scope, $element) {
			var zoomSliderWidget = $element.find('.zoom-slider-widget');

			$scope.getZoomLevels = function() {
				var zoomLevels = [];
				for (var i = $scope.config.zoomLevels - 1; i >= 0; i--) {
					zoomLevels.push(i);
				}
				return zoomLevels;
			};

			$scope.widgetConfig = {
				zoomLevelHeight : 10
			};

			$scope.zoomIn = function() {
				$scope.model.zoomIn();
			};

			$scope.zoomOut = function() {
				$scope.model.zoomOut();
			};

			$scope.zoomToLevel = function(level) {
				$scope.model.changeZoomLevel(level);
			};

			// $watch is not fast enough so we set up our own polling
			setInterval(function() {
				zoomSliderWidget.css('top', (($scope.config.zoomLevels - $scope.model.zoomLevel - 1) * $scope.widgetConfig.zoomLevelHeight) + 'px');
			}, 25);
		}],
		template:
			'
' +
				'
+
' +
				'
' +
					'

' +
					'

' +
				'
' +
				'
-
' +
			'
',
		replace: true
	};
});
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2011  JTalks.org Team
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

$(function () {
    var baseUrl = $root;

    $('.comment-prompt').click(function (e) {
        e.preventDefault();
        $(this).hide();
        $(this).next(".comment-container").show();
        $(this).next(".comment-container").children(".comment-textarea").focus();
    });

    $('.comment-cancel').click(function (e) {
        e.preventDefault();
        $(this).parents(".comment-container").children(".comment-textarea").val("");
        $(this).parents(".comment-container").hide();
        $(this).parents(".comments").children(".comment-prompt").show();
    });

    $('.expand').click(function(e){
        e.preventDefault();
        $(this).parent().prev(".comment-list").children(".togglable").toggle();
        var buttons = $(this).parent().children(".togglable");
        buttons.toggle();
        buttons.focus();
        var thirdComment = $(this).parent().prev(".comment-list").children(".hiddenBorder");
        if (thirdComment.hasClass("bordered")) {
            thirdComment.removeClass("bordered");
        } else {
            thirdComment.addClass("bordered");
        }
    });

    $(".vote-up").mouseup(voteUpHandler);

    $(".vote-down").mouseup(voteDownHandler);

    $('#answer').click(function(e){
        e.preventDefault();
        $('#postBody').focus();
    });

    if ($('#answerForm .error:visible').length > 0) {
        $('#postBody').focus();
    }
});

var voteUpHandler = function voteUp(e) {
    if (e.which != 1) {
        return;
    }
    var postId = getVotedPostId($(this).attr('id'));
    if (isOwnPost(postId)) {
        showErrorPopUp(postId, labelVoteErrorOwnPost);
        return;
    }
    if (isVotedUp(postId)) {
        return;
    }
    $(".vote-up").unbind("mouseup");
    $.ajax({
        url: baseUrl + "/posts/" + postId + "/voteup",
        type: "GET",
        success: function(data) {
            if (isVotedDown(postId)) {
                changeRating($("#" + postId + "-rating"), 2);
            } else {
                changeRating($("#" + postId + "-rating"), 1);
            }
            markVoteUpAsPressed(postId);
        },
        error: function() {
            showErrorPopUp(postId, labelVoteErrorNoPermissions);
        }
    });
    setTimeout(function() {
        $(".vote-up").mouseup(voteUpHandler);
    }, 1000);
}

var voteDownHandler = function voteDown(e) {
    if (e.which != 1) {
        return;
    }
    var postId = getVotedPostId($(this).attr('id'));
    if (isOwnPost(postId)) {
        showErrorPopUp(postId, labelVoteErrorOwnPost);
        return;
    }
    if (isVotedDown(postId)) {
        return;
    }
    $(".vote-down").unbind("mouseup");
    $.ajax({
        url: baseUrl + "/posts/" + postId + "/votedown",
        type: "GET",
        success: function() {
            if (isVotedUp(postId)) {
                changeRating($("#" + postId + "-rating"), -2);
            } else {
                changeRating($("#" + postId + "-rating"), -1);
            }
            markVoteDownAsPressed(postId);
        },
        error: function() {
            showErrorPopUp(postId, labelVoteErrorNoPermissions);
        }
    });
    setTimeout(function() {
        $(".vote-down").mouseup(voteDownHandler);
    }, 1000);
}

/**
 * Shows pop-up with vote error message
 *
 * @param postId id of post where vote error occurred
 * @param message error message to be displayed
 */
function showErrorPopUp(postId, message) {
    $('#error-message-' + postId + " > span").text(message)
    $("#error-message-" + postId).show();
    setTimeout(function() {
        $("#error-message-" + postId).hide();
    }, 2000);
}

/**
 * Determines whatever current post is created by current user
 *
 * @param postId id of post to check
 * @return {boolean} true if post with specified id was created by current user, false otherwise
 */
function isOwnPost(postId) {
    return $("#" + postId + "-rating").attr("data-viewer-is-post-owner") == "true";
}

/**
 * Increment post rating which displayed in specified span
 *
 * @param ratingSpan span with rating
 * @param value value of rating changes
 */
function changeRating(ratingSpan, value) {
    var rating = parseInt(ratingSpan.text());
    rating = rating + value;
    ratingSpan.text(rating);
    // we need apply new style only if rating state changed
    if ((rating > - 2) && (rating < 2)) {
        applyRatingStyle(ratingSpan, rating);
    }
}

/**
 * Applies css classes such as positive, negative and neutral to rating span
 *
 * @param ratingSpan span to apply classes
 * @param rating new rating value
 */
function applyRatingStyle(ratingSpan, rating) {
    ratingSpan.removeClass("positive negative");
    if (rating > 0) {
        ratingSpan.addClass("positive");
    }
    if(rating < 0) {
        ratingSpan.addClass("negative");
    }
}

/**
 * Marks vote up arrow as pressed for specified post id
 *
 * @param postId id  for post to mark arrow
 */
function markVoteUpAsPressed(postId) {
    var voteDownArrow = $("#" + postId + "-down");
    if (voteDownArrow.hasClass("vote-down-pressed")) {
        voteDownArrow.removeClass("vote-down-pressed");
        voteDownArrow.addClass("vote-down-unpressed");
    }
    var voteUpArrow = $("#" + postId + "-up");
    voteUpArrow.removeClass("vote-up-unpressed");
    voteUpArrow.addClass("vote-up-pressed");
}

/**
 * Marks vote down arrow as pressed for specified post id
 *
 * @param postId id  for post to mark arrow
 */
function markVoteDownAsPressed(postId) {
    var voteUpArrow = $("#" + postId + "-up");
    if (voteUpArrow.hasClass("vote-up-pressed")) {
        voteUpArrow.removeClass("vote-up-pressed");
        voteUpArrow.addClass("vote-up-unpressed");
    }
    var voteDownArrow = $("#" + postId + "-down");
    voteDownArrow.removeClass("vote-down-unpressed");
    voteDownArrow.addClass("vote-down-pressed");
}

/**
 * Determines whatever user voted down for post with specified id
 *
 * @param postId id of post to check
 * @returns {boolean} true if user voted down for post otherwise false
 */
function isVotedDown(postId) {
    return $("#" + postId + "-down").hasClass("vote-down-pressed");
}

/**
 * Determines whatever user voted up for post with specified id
 *
 * @param postId id of post to check
 * @returns {boolean} true if user voted up for post otherwise false
 */
function isVotedUp(postId) {
    return $("#" + postId + "-up").hasClass("vote-up-pressed");
}

/**
 * Gets id of voted post based on arrow element id
 *
 * @param elementId arrow element id
 * @return {String} id of voted post
 */
function getVotedPostId(elementId) {
    return elementId.split("-")[0];
}

$(document).mouseup(function(e) {
    if (!($(e.target).hasClass("comment-submit") || $(e.target).hasClass("comment-textarea"))) {
       hideEmptyCommentTextArea();
    }
});

$(document).keyup(function(e){
    //esc button
    if (e.keyCode == 27) {
        hideEmptyCommentTextArea();
    }
});

function hideEmptyCommentTextArea() {
    $(".comment-container").each(function() {
        if (!$(this).children(".comment-textarea").val()) {
            $(this).hide();
            $(this).parent().children(".comment-prompt").show();
        }
    });
}<commit_msg>#JC-2099 Increased threshold of rating style changing<commit_after>/*
 * Copyright (C) 2011  JTalks.org Team
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

$(function () {
    var baseUrl = $root;

    $('.comment-prompt').click(function (e) {
        e.preventDefault();
        $(this).hide();
        $(this).next(".comment-container").show();
        $(this).next(".comment-container").children(".comment-textarea").focus();
    });

    $('.comment-cancel').click(function (e) {
        e.preventDefault();
        $(this).parents(".comment-container").children(".comment-textarea").val("");
        $(this).parents(".comment-container").hide();
        $(this).parents(".comments").children(".comment-prompt").show();
    });

    $('.expand').click(function(e){
        e.preventDefault();
        $(this).parent().prev(".comment-list").children(".togglable").toggle();
        var buttons = $(this).parent().children(".togglable");
        buttons.toggle();
        buttons.focus();
        var thirdComment = $(this).parent().prev(".comment-list").children(".hiddenBorder");
        if (thirdComment.hasClass("bordered")) {
            thirdComment.removeClass("bordered");
        } else {
            thirdComment.addClass("bordered");
        }
    });

    $(".vote-up").mouseup(voteUpHandler);

    $(".vote-down").mouseup(voteDownHandler);

    $('#answer').click(function(e){
        e.preventDefault();
        $('#postBody').focus();
    });

    if ($('#answerForm .error:visible').length > 0) {
        $('#postBody').focus();
    }
});

var voteUpHandler = function voteUp(e) {
    if (e.which != 1) {
        return;
    }
    var postId = getVotedPostId($(this).attr('id'));
    if (isOwnPost(postId)) {
        showErrorPopUp(postId, labelVoteErrorOwnPost);
        return;
    }
    if (isVotedUp(postId)) {
        return;
    }
    $(".vote-up").unbind("mouseup");
    $.ajax({
        url: baseUrl + "/posts/" + postId + "/voteup",
        type: "GET",
        success: function(data) {
            if (isVotedDown(postId)) {
                changeRating($("#" + postId + "-rating"), 2);
            } else {
                changeRating($("#" + postId + "-rating"), 1);
            }
            markVoteUpAsPressed(postId);
        },
        error: function() {
            showErrorPopUp(postId, labelVoteErrorNoPermissions);
        }
    });
    setTimeout(function() {
        $(".vote-up").mouseup(voteUpHandler);
    }, 1000);
}

var voteDownHandler = function voteDown(e) {
    if (e.which != 1) {
        return;
    }
    var postId = getVotedPostId($(this).attr('id'));
    if (isOwnPost(postId)) {
        showErrorPopUp(postId, labelVoteErrorOwnPost);
        return;
    }
    if (isVotedDown(postId)) {
        return;
    }
    $(".vote-down").unbind("mouseup");
    $.ajax({
        url: baseUrl + "/posts/" + postId + "/votedown",
        type: "GET",
        success: function() {
            if (isVotedUp(postId)) {
                changeRating($("#" + postId + "-rating"), -2);
            } else {
                changeRating($("#" + postId + "-rating"), -1);
            }
            markVoteDownAsPressed(postId);
        },
        error: function() {
            showErrorPopUp(postId, labelVoteErrorNoPermissions);
        }
    });
    setTimeout(function() {
        $(".vote-down").mouseup(voteDownHandler);
    }, 1000);
}

/**
 * Shows pop-up with vote error message
 *
 * @param postId id of post where vote error occurred
 * @param message error message to be displayed
 */
function showErrorPopUp(postId, message) {
    $('#error-message-' + postId + " > span").text(message)
    $("#error-message-" + postId).show();
    setTimeout(function() {
        $("#error-message-" + postId).hide();
    }, 2000);
}

/**
 * Determines whatever current post is created by current user
 *
 * @param postId id of post to check
 * @return {boolean} true if post with specified id was created by current user, false otherwise
 */
function isOwnPost(postId) {
    return $("#" + postId + "-rating").attr("data-viewer-is-post-owner") == "true";
}

/**
 * Increment post rating which displayed in specified span
 *
 * @param ratingSpan span with rating
 * @param value value of rating changes
 */
function changeRating(ratingSpan, value) {
    var rating = parseInt(ratingSpan.text());
    rating = rating + value;
    ratingSpan.text(rating);
    // we need apply new style only if rating state changed
    // but rating value may changed by 2 if user revotes for post
    if ((rating > - 3) && (rating < 3)) {
        applyRatingStyle(ratingSpan, rating);
    }
}

/**
 * Applies css classes such as positive, negative and neutral to rating span
 *
 * @param ratingSpan span to apply classes
 * @param rating new rating value
 */
function applyRatingStyle(ratingSpan, rating) {
    ratingSpan.removeClass("positive negative");
    if (rating > 0) {
        ratingSpan.addClass("positive");
    }
    if(rating < 0) {
        ratingSpan.addClass("negative");
    }
}

/**
 * Marks vote up arrow as pressed for specified post id
 *
 * @param postId id  for post to mark arrow
 */
function markVoteUpAsPressed(postId) {
    var voteDownArrow = $("#" + postId + "-down");
    if (voteDownArrow.hasClass("vote-down-pressed")) {
        voteDownArrow.removeClass("vote-down-pressed");
        voteDownArrow.addClass("vote-down-unpressed");
    }
    var voteUpArrow = $("#" + postId + "-up");
    voteUpArrow.removeClass("vote-up-unpressed");
    voteUpArrow.addClass("vote-up-pressed");
}

/**
 * Marks vote down arrow as pressed for specified post id
 *
 * @param postId id  for post to mark arrow
 */
function markVoteDownAsPressed(postId) {
    var voteUpArrow = $("#" + postId + "-up");
    if (voteUpArrow.hasClass("vote-up-pressed")) {
        voteUpArrow.removeClass("vote-up-pressed");
        voteUpArrow.addClass("vote-up-unpressed");
    }
    var voteDownArrow = $("#" + postId + "-down");
    voteDownArrow.removeClass("vote-down-unpressed");
    voteDownArrow.addClass("vote-down-pressed");
}

/**
 * Determines whatever user voted down for post with specified id
 *
 * @param postId id of post to check
 * @returns {boolean} true if user voted down for post otherwise false
 */
function isVotedDown(postId) {
    return $("#" + postId + "-down").hasClass("vote-down-pressed");
}

/**
 * Determines whatever user voted up for post with specified id
 *
 * @param postId id of post to check
 * @returns {boolean} true if user voted up for post otherwise false
 */
function isVotedUp(postId) {
    return $("#" + postId + "-up").hasClass("vote-up-pressed");
}

/**
 * Gets id of voted post based on arrow element id
 *
 * @param elementId arrow element id
 * @return {String} id of voted post
 */
function getVotedPostId(elementId) {
    return elementId.split("-")[0];
}

$(document).mouseup(function(e) {
    if (!($(e.target).hasClass("comment-submit") || $(e.target).hasClass("comment-textarea"))) {
       hideEmptyCommentTextArea();
    }
});

$(document).keyup(function(e){
    //esc button
    if (e.keyCode == 27) {
        hideEmptyCommentTextArea();
    }
});

function hideEmptyCommentTextArea() {
    $(".comment-container").each(function() {
        if (!$(this).children(".comment-textarea").val()) {
            $(this).hide();
            $(this).parent().children(".comment-prompt").show();
        }
    });
}<|endoftext|>


<commit_before>/**
 * "Download" panel for each element in data list panel.
 * @author Roman Sutormin <rsutormin@lbl.gov>
 * @public
 */
(function( $, undefined ) {
    $.KBWidget({
        name: "kbaseNarrativeDownloadPanel",
        parent: "kbaseWidget",
        version: "1.0.0",
        options: {
        	token: null,
        	type: null,
        	wsId: null,
        	objId: null
        },
        token: null,
        type: null,
        wsId: null,
        objId: null,
        loadingImage: "static/kbase/images/ajax-loader.gif",
        wsUrl: window.kbconfig.urls.workspace,
        transformURL: window.kbconfig.urls.transform,
        ujsURL: window.kbconfig.urls.user_and_job_state,
        shockURL: window.kbconfig.urls.shock,
        exportURL: window.kbconfig.urls.data_import_export,
        timer: null,

        downloaders: {  // type -> {name: ..., external_type: ...[, transform_options: ...[, unzip: <file_ext>]}
        	'KBaseGenomes.ContigSet': [{name: 'FASTA', external_type: 'FASTA.DNA.Assembly', transform_options: {output_file_name: 'fs.fasta'}}],
        	'KBaseGenomes.Genome': [{name: "GENBANK", external_type: 'Genbank.Genome', transform_options: {}}],
        	'KBaseAssembly.SingleEndLibrary': [{name: "FASTQ", external_type: 'SequenceReads', transform_options: {output_file_name: '?.fastq'}}]
        },

        init: function(options) {
            this._super(options);
            this.token = this.options.token;
            this.type = this.options.type;
            this.wsId = this.options.wsId;
            this.objId = this.options.objId;
            this.render();
            return this;
        },

        render: function() {
            var self = this;
    		var downloadPanel = this.$elem;
		downloadPanel.append('Export as:');
    		var addDownloader = function(descr) {
    		downloadPanel.append($('<button>').addClass('kb-data-list-btn')
    					.append(descr.name)
    					.click(function() {
						downloadPanel.find('.kb-data-list-btn').prop('disabled', true);
    						self.runDownloader(self.type, self.wsId, self.objId, descr);
    					}));
    		};
    		var downloaders = self.prepareDownloaders(self.type, self.wsId, self.objId);
    		for (var downloadPos in downloaders)
    			addDownloader(downloaders[downloadPos]);

    		downloadPanel.append($('<button>').addClass('kb-data-list-btn')
                    .append('JSON')
                    .click(function() {
                    	var url = self.exportURL + '/download?ws='+self.wsId+'&id='+self.objId+'&token='+self.token+
                    		'&url='+encodeURIComponent(self.wsUrl);
                    	self.downloadFile(url);
                    }));
    		downloadPanel.append($('<button>').addClass('kb-data-list-cancel-btn')
                    .append('Cancel')
                    .click(function() {
			self.stopTimer();
			downloadPanel.empty();
		    } ));

	    self.$statusDiv = $('
').css({'margin':'15px'});
	    self.$statusDivContent = $('
');
	    self.$statusDiv.append(self.$statusDivContent);
	    downloadPanel.append(self.$statusDiv.hide());
        },

        prepareDownloaders: function(type, wsId, objId) {
        	var descrList = this.downloaders[type];
        	var ret = [];
        	for (var descrPos in descrList) {
        		var descr = descrList[descrPos];
        		var retDescr = {name: descr.name, external_type: descr.external_type, unzip: descr.unzip};
        		ret.push(retDescr);
        		if (descr.transform_options) {
        			retDescr.transform_options = {};
        			for (var key in descr.transform_options) {
        				if (!descr.transform_options.hasOwnProperty(key))
        					continue;
        				var value = descr.transform_options[key];
        				if (value.indexOf('?') == 0)
        					value = objId + value.substring(1);
        				retDescr.transform_options[key] = value;
        			}
        		}
        	}
        	return ret;
        },

        runDownloader: function(type, wsId, objId, descr) { // descr is {name: ..., external_type: ...[, transform_options: ...[, unzip: ...]]}
            var self = this;
            self.showMessage(' Export status: Preparing data');
            self.$statusDiv.show();
        	var transform_options = descr.transform_options;
        	if (!transform_options)
        		transform_options = {};
        	var args = {external_type: descr.external_type, kbase_type: type, workspace_name: wsId, object_name: objId, optional_arguments: {transform: transform_options}};
    		console.log("Downloader data to be sent to transform service:");
    		console.log(JSON.stringify(args));
            var transformSrv = new Transform(this.transformURL, {token: this.token});
            transformSrv.download(args,
            		$.proxy(function(data) {
            			console.log(data);
            			var jobId = data[1];
            			self.waitForJob(jobId, objId, descr.unzip);
            		}, this),
            		$.proxy(function(data) {
            			console.log(data.error.error);
            			self.showError(data.error.error);
            		}, this)
            );
        },

        waitForJob: function(jobId, wsObjectName, unzip) {
            var self = this;
            var jobSrv = new UserAndJobState(this.ujsURL, {token: this.token});
			var timeLst = function(event) {
				jobSrv.get_job_status(jobId, function(data) {
					//console.log(data);
					var status = data[2];
					var complete = data[5];
					var wasError = data[6];
					if (complete === 1) {
						self.stopTimer();
						if (wasError === 0) {
							console.log("Export is complete");
							// Starting download from Shock
							jobSrv.get_results(jobId, function(data) {
								self.$statusDiv.hide();
								self.$elem.find('.kb-data-list-btn').prop('disabled', false);
								console.log(data);
								self.downloadUJSResults(data, wsObjectName, unzip);
							}, function(data) {
            					console.log(data.error.message);
                    			self.showError(data.error.message);
							});
						} else {
							console.log(status);
	            			self.showError(status);
						}
					} else {
						console.log("Export status: " + status, true);
			            self.showMessage(' Export status: ' + status);
					}
				}, function(data) {
					self.stopTimer();
					console.log(data.error.message);
        			self.showError(data.error.message);
				});
			};
			self.timer = setInterval(timeLst, 5000);
			timeLst();
        },

        downloadUJSResults: function(ujsResults, wsObjectName, unzip) {
        	var self = this;
			var shockNode = ujsResults.shocknodes[0];
			var elems = shockNode.split('/');
			if (elems.length > 1)
				shockNode = elems[elems.length - 1];
			elems = shockNode.split('?');
			if (elems.length > 0)
				shockNode = elems[0];
			console.log("Shock node ID: " + shockNode);
        	var shockClient = new ShockClient({url: self.shockURL, token: self.token});
        	var downloadShockNodeWithName = function(name) {
    			var url = self.exportURL + '/download?id='+shockNode+'&token='+
    				encodeURIComponent(self.token)+'&del=1';
    			if (unzip) {
    				url += '&unzip='+encodeURIComponent(unzip);
    			} else {
    				url += '&name='+encodeURIComponent(name);
    			}
    			var remoteShockUrl = ujsResults.shockurl;
    			if (remoteShockUrl)
    				url += '&url='+encodeURIComponent(remoteShockUrl);
    			self.downloadFile(url);
        	};
        	/*shockClient.get_node(shockNode, function(data) {
        		console.log(data);
        		downloadShockNodeWithName(data.file.name);
        	}, function(error) {
        		console.log(error);
        	});*/
        	downloadShockNodeWithName(wsObjectName + ".zip");
        },

        downloadFile: function(url) {
        	console.log("Downloading url=" + url);
        	var hiddenIFrameID = 'hiddenDownloader';
            var iframe = document.getElementById(hiddenIFrameID);
        	if (iframe === null) {
        		iframe = document.createElement('iframe');
        		iframe.id = hiddenIFrameID;
        		iframe.style.display = 'none';
        		document.body.appendChild(iframe);
        	}
        	iframe.src = url;
        },

        showMessage: function(msg) {
        	var self = this;
            self.$statusDivContent.empty();
            self.$statusDivContent.append(msg);
        },

        showError: function(msg) {
        	var self = this;
		self.$statusDivContent.empty();
		self.$statusDivContent.append($('').css({color:'#F44336'}).append('Error: '+msg));
        },

        stopTimer: function() {
			if (this.timer != null) {
				clearInterval(this.timer);
				this.timer = null;
				console.log("Timer was stopped");
			}
		}
    });
})( jQuery );
<commit_msg>More fixes in download UI.<commit_after>/**
 * "Download" panel for each element in data list panel.
 * @author Roman Sutormin <rsutormin@lbl.gov>
 * @public
 */
(function( $, undefined ) {
    $.KBWidget({
        name: "kbaseNarrativeDownloadPanel",
        parent: "kbaseWidget",
        version: "1.0.0",
        options: {
        	token: null,
        	type: null,
        	wsId: null,
        	objId: null
        },
        token: null,
        type: null,
        wsId: null,
        objId: null,
        loadingImage: "static/kbase/images/ajax-loader.gif",
        wsUrl: window.kbconfig.urls.workspace,
        transformURL: window.kbconfig.urls.transform,
        ujsURL: window.kbconfig.urls.user_and_job_state,
        shockURL: window.kbconfig.urls.shock,
        exportURL: window.kbconfig.urls.data_import_export,
        timer: null,

        downloaders: {  // type -> {name: ..., external_type: ...[, transform_options: ...[, unzip: <file_ext>]}
        	'KBaseGenomes.ContigSet': [{name: 'FASTA', external_type: 'FASTA.DNA.Assembly', transform_options: {output_file_name: 'fs.fasta'}}],
        	'KBaseGenomes.Genome': [{name: "GENBANK", external_type: 'Genbank.Genome', transform_options: {}}],
        	'KBaseAssembly.SingleEndLibrary': [{name: "FASTQ", external_type: 'SequenceReads', transform_options: {output_file_name: '?.fastq'}}]
        },

        init: function(options) {
            this._super(options);
            this.token = this.options.token;
            this.type = this.options.type;
            this.wsId = this.options.wsId;
            this.objId = this.options.objId;
            this.render();
            return this;
        },

        render: function() {
            var self = this;
    		var downloadPanel = this.$elem;
		downloadPanel.append('Export as:');
    		var addDownloader = function(descr) {
    		downloadPanel.append($('<button>').addClass('kb-data-list-btn')
    					.append(descr.name)
    					.click(function() {
						downloadPanel.find('.kb-data-list-btn').prop('disabled', true);
    						self.runDownloader(self.type, self.wsId, self.objId, descr);
    					}));
    		};
    		var downloaders = self.prepareDownloaders(self.type, self.wsId, self.objId);
    		for (var downloadPos in downloaders)
    			addDownloader(downloaders[downloadPos]);

    		downloadPanel.append($('<button>').addClass('kb-data-list-btn')
                    .append('JSON')
                    .click(function() {
                    	var url = self.exportURL + '/download?ws='+self.wsId+'&id='+self.objId+'&token='+self.token+
                    		'&url='+encodeURIComponent(self.wsUrl) + "&wszip=1";
                    	self.downloadFile(url);
                    }));
    		downloadPanel.append($('<button>').addClass('kb-data-list-cancel-btn')
                    .append('Cancel')
                    .click(function() {
			self.stopTimer();
			downloadPanel.empty();
		    } ));

	    self.$statusDiv = $('
').css({'margin':'15px'});
	    self.$statusDivContent = $('
');
	    self.$statusDiv.append(self.$statusDivContent);
	    downloadPanel.append(self.$statusDiv.hide());
        },

        prepareDownloaders: function(type, wsId, objId) {
        	var descrList = this.downloaders[type];
        	var ret = [];
        	for (var descrPos in descrList) {
        		var descr = descrList[descrPos];
        		var retDescr = {name: descr.name, external_type: descr.external_type, unzip: descr.unzip};
        		ret.push(retDescr);
        		if (descr.transform_options) {
        			retDescr.transform_options = {};
        			for (var key in descr.transform_options) {
        				if (!descr.transform_options.hasOwnProperty(key))
        					continue;
        				var value = descr.transform_options[key];
        				if (value.indexOf('?') == 0)
        					value = objId + value.substring(1);
        				retDescr.transform_options[key] = value;
        			}
        		}
        	}
        	return ret;
        },

        runDownloader: function(type, wsId, objId, descr) { // descr is {name: ..., external_type: ...[, transform_options: ...[, unzip: ...]]}
            var self = this;
            self.showMessage(' Export status: Preparing data');
            self.$statusDiv.show();
        	var transform_options = descr.transform_options;
        	if (!transform_options)
        		transform_options = {};
        	var args = {external_type: descr.external_type, kbase_type: type, workspace_name: wsId, object_name: objId, optional_arguments: {transform: transform_options}};
    		console.log("Downloader data to be sent to transform service:");
    		console.log(JSON.stringify(args));
            var transformSrv = new Transform(this.transformURL, {token: this.token});
            transformSrv.download(args,
            		$.proxy(function(data) {
            			console.log(data);
            			var jobId = data[1];
            			self.waitForJob(jobId, objId, descr.unzip);
            		}, this),
            		$.proxy(function(data) {
            			console.log(data.error.error);
            			self.showError(data.error.error);
            		}, this)
            );
        },

        waitForJob: function(jobId, wsObjectName, unzip) {
            var self = this;
            var jobSrv = new UserAndJobState(this.ujsURL, {token: this.token});
			var timeLst = function(event) {
				jobSrv.get_job_status(jobId, function(data) {
					//console.log(data);
					var status = data[2];
					var complete = data[5];
					var wasError = data[6];
					if (complete === 1) {
						self.stopTimer();
						if (wasError === 0) {
							console.log("Export is complete");
							// Starting download from Shock
							jobSrv.get_results(jobId, function(data) {
								self.$statusDiv.hide();
								self.$elem.find('.kb-data-list-btn').prop('disabled', false);
								console.log(data);
								self.downloadUJSResults(data, wsObjectName, unzip);
							}, function(data) {
            					console.log(data.error.message);
                    			self.showError(data.error.message);
							});
						} else {
							console.log(status);
	            			self.showError(status);
						}
					} else {
						console.log("Export status: " + status, true);
			            self.showMessage(' Export status: ' + status);
					}
				}, function(data) {
					self.stopTimer();
					console.log(data.error.message);
        			self.showError(data.error.message);
				});
			};
			self.timer = setInterval(timeLst, 5000);
			timeLst();
        },

        downloadUJSResults: function(ujsResults, wsObjectName, unzip) {
        	var self = this;
			var shockNode = ujsResults.shocknodes[0];
			var elems = shockNode.split('/');
			if (elems.length > 1)
				shockNode = elems[elems.length - 1];
			elems = shockNode.split('?');
			if (elems.length > 0)
				shockNode = elems[0];
			console.log("Shock node ID: " + shockNode);
        	var shockClient = new ShockClient({url: self.shockURL, token: self.token});
        	var downloadShockNodeWithName = function(name) {
    			var url = self.exportURL + '/download?id='+shockNode+'&token='+
    				encodeURIComponent(self.token)+'&del=1';
    			if (unzip) {
    				url += '&unzip='+encodeURIComponent(unzip);
    			} else {
    				url += '&name='+encodeURIComponent(name);
    			}
    			var remoteShockUrl = ujsResults.shockurl;
    			if (remoteShockUrl)
    				url += '&url='+encodeURIComponent(remoteShockUrl);
    			self.downloadFile(url);
        	};
        	/*shockClient.get_node(shockNode, function(data) {
        		console.log(data);
        		downloadShockNodeWithName(data.file.name);
        	}, function(error) {
        		console.log(error);
        	});*/
        	downloadShockNodeWithName(wsObjectName + ".zip");
        },

        downloadFile: function(url) {
        	console.log("Downloading url=" + url);
        	var hiddenIFrameID = 'hiddenDownloader';
            var iframe = document.getElementById(hiddenIFrameID);
        	if (iframe === null) {
        		iframe = document.createElement('iframe');
        		iframe.id = hiddenIFrameID;
        		iframe.style.display = 'none';
        		document.body.appendChild(iframe);
        	}
        	iframe.src = url;
        },

        showMessage: function(msg) {
        	var self = this;
            self.$statusDivContent.empty();
            self.$statusDivContent.append(msg);
        },

        showError: function(msg) {
        	var self = this;
		self.$statusDivContent.empty();
		self.$statusDivContent.append($('').css({color:'#F44336'}).append('Error: '+msg));
        },

        stopTimer: function() {
			if (this.timer != null) {
				clearInterval(this.timer);
				this.timer = null;
				console.log("Timer was stopped");
			}
		}
    });
})( jQuery );
<|endoftext|>


<commit_before>var stream = require('stream')
var util = require('util')
var cadence = require('cadence')
var delta = require('delta')
var interrupt = require('interrupt').createInterrupter('staccato')
var Staccato = require('./base.js')

function Readable (stream, opening) {
    Staccato.call(this, stream, opening)
    this._listeners.end = this._end.bind(this)
    this.stream.once('end', this._listeners.end)
    this._readable = true
}
util.inherits(Readable, Staccato)

Readable.prototype.destroy = function () {
    this.stream.removeListener('end', this._listeners.end)
    Staccato.prototype.destroy.call(this)
}

Readable.prototype._end = function () {
    this._destroy([])
}

Readable.prototype.read = cadence(function (async, count) {
    var loop = async(function () {
        if (!this._readable) {
            this._delta = delta(async()).ee(this.stream).on('readable')
        }
    }, function () {
        this._delta = null

        this._readable = true

        if (this.destroyed) {
            if (this._error != null) {
                throw error
            }

            // Unlike Writable, reading a closed Readable will always return
            // null no matter how often you call it.
            return [ loop.break, null ]
        }

        var object = count == null ? this.stream.read() : this.stream.read(count)
        if (object == null) {
            this._readable = false
        } else {
            return [ loop.break, object ]
        }
    })()
})

module.exports = Readable
<commit_msg>Notes on readable end.<commit_after>var stream = require('stream')
var util = require('util')
var cadence = require('cadence')
var delta = require('delta')
var interrupt = require('interrupt').createInterrupter('staccato')
var Staccato = require('./base.js')

function Readable (stream, opening) {
    Staccato.call(this, stream, opening)
    this._listeners.end = this._end.bind(this)
    this.stream.once('end', this._listeners.end)
    this._readable = true
}
util.inherits(Readable, Staccato)

Readable.prototype.destroy = function () {
    this.stream.removeListener('end', this._listeners.end)
    Staccato.prototype.destroy.call(this)
}

// The `end` event will issued until `end` is called and all the data has been
// read from the stream. Calling destroy here will not cause any sort of
// truncation doing so explicitly cancels our wait on the `readable` event.
//
// Basically, we know that during normal operation, after we've reached the end
// of stream our `read` method is going to get that last `readable` that says
// that the buffer has drained and then loop back around and wait on `readable`
// again, blocking indefinitely unless we cancel its wait via Delta.

//
Readable.prototype._end = function () {
    this._destroy([])
}

Readable.prototype.read = cadence(function (async, count) {
    var loop = async(function () {
        if (!this._readable) {
            this._delta = delta(async()).ee(this.stream).on('readable')
        }
    }, function () {
        this._delta = null

        this._readable = true

        if (this.destroyed) {
            if (this._error != null) {
                throw error
            }

            // Unlike Writable, reading a closed Readable will always return
            // null no matter how often you call it.
            return [ loop.break, null ]
        }

        var object = count == null ? this.stream.read() : this.stream.read(count)
        if (object == null) {
            this._readable = false
        } else {
            return [ loop.break, object ]
        }
    })()
})

module.exports = Readable
<|endoftext|>