ui/controllers/dashboard_controller.js

const $ = require('jquery');
const { Context } = require('../../core/context');
const dateFormat = require('dateformat');
const { Job } = require('../../core/job');
const { PluginManager } = require('../../plugins/plugin_manager');
const { RemoteRepository } = require('../../core/remote_repository');
const { RunningJobsController } = require('./running_jobs_controller');
const Templates = require('../common/templates');
const { Util } = require('../../core/util');

/**
 * The Dashboard crontroller displays information about recent
 * jobs, recent uploads, etc.
 *
 */
class DashboardController extends RunningJobsController {

    constructor(params) {
        super(params, 'Dashboard')
    }

    /**
     * This collects and renders all of the data to be displayed
     * in the Dashboard. The data includes information about currently
     * running jobs, recently defined jobs, and information fetched
     * from any remote repositories DART can connect to.
     *
     */
    show() {
        // Get a list of RemoteRepository clients that have enough information
        // to attempt a connection with the remote repo.
        let clients = this._getViableRepoClients();

        // Get a list of available reports.
        let repoReports = this._getRepoReportDescriptions(clients);

        // Each of these clients can provide one or more reports. We want to
        // display the reports in rows, with two reports per row.
        let reportRows = this._getRepoRows(repoReports);

        // Assemble the HTML
        let html = Templates.dashboard({
            alertMessage: this.params.get('alertMessage'),
            reportRows: reportRows,
            runningJobs: Object.values(Context.childProcesses),
            recentJobs: this._getRecentJobSummaries()
        });

        // Call the reports. These are asynchronous, since they have to
        // query remote repositories. The callbacks describe which HTML
        // element on the rendered page will contain the HTML output of
        // the reports.
        repoReports.forEach((report) => {
            let elementId = '#' + report.id
            report.method().then(
                result => {
                    $(elementId).removeClass('text-danger');
                    $(elementId).html(result)
                },
                error => {
                    $(elementId).addClass('text-danger');
                    $(elementId).html(error)
                }
            );
        });

        return this.containerContent(html);
    }

    /**
     * This returns summary info about the ten most recent jobs.
     * The return value is an array of objects, each of which has three
     * string properties. Object.name is the job name. Object.outcome
     * is the name and outcome of the last attempted action.
     * Object.date is the date at which the job last completed.
     *
     * @returns {Array<object>}
     */
    _getRecentJobSummaries() {
        let jobSummaries = [];
        // This sort can approximate the 20 most recent jobs,
        // but the sort below, based on a calculated value, is more accurate.
        let opts = {limit: 20, offset: 0, orderBy: 'createdAt', sortDir: 'desc'};
        // TODO: Override list() in Job to do its own inflation?
        let jobs = Job.list(null, opts).map((data) => { return Job.inflateFrom(data) });
        for (let job of jobs) {
            let [outcome, timestamp] = this._getJobOutcomeAndTimestamp(job);
            jobSummaries.push({
                name: job.title,
                outcome: outcome,
                date: timestamp
            });
        }
        let sortFn = Util.getSortFunction('date', 'desc');
        return jobSummaries.sort(sortFn);
    }

    /**
     * This returns formatted information about the outcome of a job
     * and when that outcome occurred. The return value is a two-element
     * array of strings. The first element describes the outcome. The
     * second is the date on which the outcome occurred.
     *
     * @returns {Array<string>}
     *
     */
    _getJobOutcomeAndTimestamp(job) {
        // TODO: This code has some overlap with JobController#colorCodeJobs.
        let outcome = "Job has not been run.";
        let timestamp = null;
        if(job.uploadAttempted()) {
            outcome = job.uploadSucceeded() ? 'Uploaded' : 'Upload failed';
            timestamp = dateFormat(job.uploadedAt(), 'yyyy-mm-dd');
        } else if (job.validationAttempted) {
            outcome = job.validationSucceeded() ? 'Validated' : 'Validation failed';
            timestamp = dateFormat(job.validatedAt(), 'yyyy-mm-dd');
        } else if (job.packageAttempted()) {
            outcome = job.packageSucceeded() ? 'Packaged' : 'Packaging failed';
            timestamp = dateFormat(job.packagedAt(), 'yyyy-mm-dd');
        }
        return [outcome, timestamp]
    }

    /**
     * This returns a list of repository clients that look like they
     * can return data from remote repository. To find a list of viable
     * repository clients, DART first gets a list of all
     * {@link RemoteRepository} objects. If the RemoteRepository object
     * includes a {@link RemoteRepository#pluginId}, this function creates
     * an instance of the plugin and calls the plugin's {@link
     * RepositoryBase#hasRequiredConnectionInfo} method. If the method
     * returns true, the instantiated client will be returned in the list
     * of viable repo clients.
     *
     * @returns {Array<RepositoryBase>}
     *
     */
    _getViableRepoClients() {
        let repoClients = [];
        let repos = RemoteRepository.list((r) => { return !Util.isEmpty(r.pluginId) });
        for (let _repoData of repos) {
            // TODO: This object inflation should be pushed down into the
            // RemoteRepository class.
            let repo = new RemoteRepository();
            Object.assign(repo, _repoData);
            let clientClass = PluginManager.findById(repo.pluginId);
            let clientInstance = new clientClass(repo);
            if (clientInstance.hasRequiredConnectionInfo()) {
                repoClients.push(clientInstance);
            }
        }
        return repoClients;
    }

    /**
     * This returns a list of report rows to be displayed in the dashboard.
     * Each row has up to two reports. (Because we're using the brain dead
     * Handlebars templating library, we have to format our data structures
     * precisely before rendering them. And, by the way, templates *should*
     * be brain dead, because when they start thinking, they become evil,
     * like PHP and React.)
     *
     * @param {} repoReports - A list of repo report summary objects, which
     * can be obtained from
     * {@link DashboardController#_getRepoReportDescriptions}.
     *
     * @returns {Array<Array<object>>}
     */
    _getRepoRows(repoReports) {
        let reportRows = [];
        let i = 0;
        while (i < repoReports.length) {
            let report1 = repoReports[i];
            let report2 = i + 1 < repoReports.length ? repoReports[i + 1] : null;
            reportRows.push([report1, report2]);
            i += 2;
        }
        return reportRows;
    }

    /**
     * This returns an array of objects describing all of the
     * reports available from all of the viable repository clients.
     *
     * Internally, this calls {@link RepositoryBase#provides} to
     * get the title, description, and function to call for each report.
     *
     * This adds an id to each description record, so the Dashboard
     * controller can map each report to the HTML element that will
     * display its output.
     *
     * Each item in the returned array will have properies id, title,
     * description, and method.
     *
     * @returns {Array<object>}
     *
     */
    _getRepoReportDescriptions(clients) {
        let reports = [];
        let clientNumber = 0;
        clients.forEach((client) => {
            clientNumber++;
            let className = client.constructor.name;
            let reportIndex = 0;
            client.provides().forEach((report) => {
                reportIndex++;
                reports.push({
                    id: `${className}_${clientNumber}_${reportIndex}`,
                    title: report.title,
                    description: report.description,
                    method: report.method
                });
            }
        )});
        return reports;
    }

    postRenderCallback(fnName) {
        for(let dartProcess of Object.values(Context.childProcesses)) {
            this.initRunningJobDisplay(dartProcess);
        }
    }

}

module.exports.DashboardController = DashboardController;