<commit_before>import APIClient from "./apiclient";
import {getId, getDuration, getPoints, getYoutubeId} from "./data/topic-tree-helper";
import _ from "underscore";
import Util from "./util";
import {editorForPath} from "./renderer";
import Immutable from "immutable";

const userInfoLocalStorageName = "userInfo-3";

/**
 * Signs the user in by redirecting them to the KA auth page
 */
export const signIn = () => APIClient.signIn();

/**
 * Signs the user out and resets user data back to empty
 */
export const signOut = (editUser) => {
    localStorage.removeItem(userInfoLocalStorageName);

    editUser((user) => user.merge({
        userInfo: null,
        startedEntityIds: [],
        completedEntityIds: [],
    }));

    return APIClient.signOut();
};

/**
 * Determines if a user is signed in or not
 */
export const isSignedIn = () =>
    APIClient.isSignedIn();

const getLocalStorageName = (base, userInfo) =>
    base + "-uid-" + (userInfo.get("nickname") || userInfo.get("username"));

const completedEntityIdsLocalStorageName = _.partial(getLocalStorageName, "completed");
const startedEntityIdsLocalStorageName = _.partial(getLocalStorageName, "started");
const userVideosLocalStorageName = _.partial(getLocalStorageName, "userVideos");
const userExercisesLocalStorageName = _.partial(getLocalStorageName, "userExercises");

/**
 * Saves a userInfo object (not ImmutableJS)
 */
const saveUserInfo = (userInfo) => {
    localStorage.removeItem(userInfoLocalStorageName);
    localStorage.setItem(userInfoLocalStorageName, JSON.stringify(userInfo));
};

/**
 * Saves a list of started entities (not ImmutableJS)
 */
const saveStarted = (userInfo, startedEntityIds) => {
    localStorage.removeItem(startedEntityIdsLocalStorageName(userInfo));
    localStorage.setItem(startedEntityIdsLocalStorageName(userInfo), JSON.stringify(startedEntityIds));
};

/**
 * Saves a list of completed entities (not ImmutableJS)
 */
const saveCompleted = (userInfo, completedEntityIds) => {
    localStorage.removeItem(completedEntityIdsLocalStorageName(userInfo));
    localStorage.setItem(completedEntityIdsLocalStorageName(userInfo), JSON.stringify(completedEntityIds));
};

/**
 * Saves a list of user entities (not ImmutableJS).
 * THis is mostly used for tracking the last position of the watched video.
 */
const saveUserVideos = (userInfo, userVideos) => {
    userVideos = userVideos.map((userVideo) => {
        return {
            duration: userVideo.duration,
            last_second_watched: userVideo.last_second_watched,
            points: userVideo.points,
            video: {
                id: userVideo.id
            }
        };
    });
    localStorage.removeItem(userVideosLocalStorageName(userInfo));
    localStorage.setItem(userVideosLocalStorageName(userInfo), JSON.stringify(userVideos));
};

/**
 * Saves a list of information on the exercise.
 */
const saveUserExercises = (userInfo, userExercises) => {
    userExercises = userExercises.map((exercise) => {
        return {
            streak: exercise.streak,
            total_correct: exercise.total_correct,
            total_done: exercise.total_done,
            exercise_model: {
                content_id: exercise.exercise_model.content_id
            }
        };
    });

    // The extra removeItem calls before the setItem calls help in case local storage is almost full
    localStorage.removeItem(userExercisesLocalStorageName(userInfo));
    localStorage.setItem(userExercisesLocalStorageName(userInfo), JSON.stringify(userExercises));
};

/**
 * Loads all of the different types of user data from local storage.
 */
const loadLocalStorageData = (userInfo) => {
    var result = {};
    var completedEntityIds = localStorage.getItem(completedEntityIdsLocalStorageName(userInfo));
    if (completedEntityIds) {
        result.completedEntityIds = JSON.parse(completedEntityIds);
    }
    var startedEntityIds = localStorage.getItem(startedEntityIdsLocalStorageName(userInfo));
    if (startedEntityIds) {
        result.startedEntityIds = JSON.parse(startedEntityIds);
    }
    var userVideos = localStorage.getItem(userVideosLocalStorageName(userInfo));
    if (userVideos) {
        result.userVideos = JSON.parse(userVideos);
    }
    var userExercises = localStorage.getItem(userExercisesLocalStorageName(userInfo));
    if (userExercises) {
        result.userExercises = JSON.parse(userExercises);
    }

    return result;
};

/**
 * Reports video progress for the user back to KA
 */
export const reportVideoProgress = (user, topicTreeNode, secondsWatched, lastSecondWatched, editVideo, editUser) => {
    return new Promise((resolve, reject) => {
        const youTubeId = getYoutubeId(topicTreeNode),
            videoId = getId(topicTreeNode),
            duration = getDuration(topicTreeNode),
            editCompletedEntityIds = editorForPath(editUser, "completedEntityIds"),
            editStartedEntityIds = editorForPath(editUser, "startedEntityIds"),
            editUserInfo = editorForPath(editUser, "userInfo"),
            editUserVideos = editorForPath(editUser, "userVideos");
        let completedEntityIds = user.get("completedEntityIds"),
            startedEntityIds = user.get("startedEntityIds"),
            userVideos = user.get("userVideos");

        APIClient.reportVideoProgress(videoId, youTubeId, secondsWatched, lastSecondWatched).then((result) => {
            if (!result) {
                Util.warn("Video progress report returned null results!");
                return;
            }
            Util.log("reportVideoProgress result: %o", result);

            var lastPoints = getPoints(topicTreeNode);
            var newPoints = lastPoints + result.points_earned;
            if (newPoints > 750) {
                newPoints = 750;
            }

            // If they've watched some part of the video, and it's not almost the end
            // Otherwise check if we already have video progress for this item and we
            // therefore no longer need it.
            var lastSecondWatched;
            if (result.last_second_watched > 10 &&
                    duration - result.last_second_watched > 10) {
                lastSecondWatched = result.last_second_watched;
            }

            // If we're just getting a completion of a video update
            // the user's overall points locally.
            if (result.points_earned > 0) {
                var userInfo = user.get("userInfo");
                userInfo = userInfo.set("points",
                    userInfo.get("points") + result.points_earned);
                editUserInfo(() => userInfo);
                saveUserInfo(userInfo);
            }

            editVideo((video) =>
                video.merge({
                    points: newPoints,
                    completed: result.is_video_completed,
                    started: !result.is_video_completed,
                    lastSecondWatched: lastSecondWatched
                }));

            // Update locally stored cached info
            if (result.is_video_completed) {
                if (startedEntityIds.contains(getId(topicTreeNode))) {
                    startedEntityIds = startedEntityIds.remove(startedEntityIds.indexOf(getId(topicTreeNode)));
                    editStartedEntityIds(() => startedEntityIds);
                }
                if (!completedEntityIds.contains(getId(topicTreeNode))) {
                    completedEntityIds = completedEntityIds.unshift(getId(topicTreeNode));
                    editCompletedEntityIds(() => completedEntityIds);
                }
            } else {
                if (!startedEntityIds.contains(getId(topicTreeNode))) {
                    startedEntityIds = startedEntityIds.unshift(getId(topicTreeNode));
                    editStartedEntityIds(() => startedEntityIds);
                }
            }

            var foundUserVideo = userVideos.find((info) =>
                info.getIn(["video", "id"]) === getId(topicTreeNode));
            if (foundUserVideo) {
                foundUserVideo = foundUserVideo.toJS();
            }
            var isNew = !foundUserVideo;
            foundUserVideo = foundUserVideo || {
                video: {
                    id: getId(topicTreeNode)
                },
                duration: getDuration(topicTreeNode)
            };
            foundUserVideo["points"] = newPoints;
            foundUserVideo["last_second_watched"] = lastSecondWatched;
            if (isNew) {
                userVideos = userVideos.unshift(Immutable.fromJS(foundUserVideo));
                editUserVideos(() => userVideos);
            }

            saveStarted(user.get("userInfo"), startedEntityIds);
            saveCompleted(user.get("userInfo"), completedEntityIds);
            saveUserVideos(user.get("userInfo"), userVideos);

            resolve({
                completed: result.is_video_completed,
                lastSecondWatched: result.last_second_watched,
                pointsEarned: result.points_earned,
                youtubeId: result.youtube_id,
                videoId,
                id: getId(topicTreeNode)
            });
        }).catch(() => {
            reject();
        });
    });
};

/**
 * Reports article progress for the user back to KA
 */
export const reportArticleRead = (user, topicTreeNode, editUser) => {
    return new Promise((resolve, reject) => {
        if (!isSignedIn()) {
            return setTimeout(resolve, 0);
        }

        APIClient.reportArticleRead(getId(topicTreeNode)).then((result) => {
            Util.log("reported article complete: %o", result);
            let completedEntityIds = user.get("completedEntityIds");
            if (!completedEntityIds.contains(getId(topicTreeNode))) {
              completedEntityIds = completedEntityIds.unshift(getId(topicTreeNode));
              const editCompletedEntityIds = editorForPath(editUser, "completedEntityIds");
              editCompletedEntityIds(() => completedEntityIds);
            }
            saveCompleted(user.get("userInfo"), completedEntityIds);
            resolve(result);
        }).catch(() => {
            reject();
        });
    });
};

/**
 * Refreshes the logged in info either from local storage or from the server
 */
export const refreshLoggedInInfo = (user, editUser, forceRefreshAllInfo) => {
    return new Promise((resolve, reject) => {
        if (!isSignedIn()) {
            return resolve();
        }

        // Get the user profile info
        APIClient.getUserInfo().then((result) => {
            Util.log("getUserInfo: %o", result);
            const userInfo = Immutable.fromJS({
                avatarUrl: result.avatar_url,
                joined: result.joined,
                nickname: result.nickname,
                username: result.username,
                points: result.points,
                badgeCounts: result.badge_counts
            });

            editUser((user) => user.set("userInfo", userInfo));

            saveUserInfo(userInfo.toJS());

            var result = loadLocalStorageData(userInfo);
            if (!forceRefreshAllInfo && result.completedEntityIds) {
                Util.log("User info only obtained. Not obtaining user data because we have it cached already!");
                return result;
            }

            // The call is needed for completed/in progress status of content items
            // Unlike getUserVideos, this includes both articles and videos.
            return APIClient.getUserProgress();

        }).then((data) => {
            Util.log("getUserProgress: %o", data);
            // data.complete will be returned from the API but not when loaded
            // directly from localStorage.  When loaded directly from localstorage
            // you'll already have result.startedEntityIds.
            if (data.complete) {
                // Get rid of the 'a' and 'v' prefixes, and set the completed / started
                // attributes accordingly.
                data.startedEntityIds = _.map(data.started, function(e) {
                    return e.substring(1);
                });
                data.completedEntityIds = _.map(data.complete, function(e) {
                    return e.substring(1);
                });

                // Save to local storage
                saveStarted(user.get("userInfo"), data.startedEntityIds);
                saveCompleted(user.get("userInfo"), data.completedEntityIds);
            }

            editUser((user) => user.merge({
                startedEntityIds: data.startedEntityIds,
                completedEntityIds: data.completedEntityIds,
            }));

            return APIClient.getUserVideos();
        }).then((userVideosResults) => {
            saveUserVideos(user.get("userInfo"), userVideosResults);
            return APIClient.getUserExercises();
        }).then((userExercisesResults) => {
            saveUserExercises(user.get("userInfo"), userExercisesResults);
            resolve();
        }).catch(() => {
            reject();
        });
    });
};
<commit_msg>Reload from server if either of started or completed entities are missing<commit_after>import APIClient from "./apiclient";
import {getId, getDuration, getPoints, getYoutubeId} from "./data/topic-tree-helper";
import _ from "underscore";
import Util from "./util";
import {editorForPath} from "./renderer";
import Immutable from "immutable";

const userInfoLocalStorageName = "userInfo-3";

/**
 * Signs the user in by redirecting them to the KA auth page
 */
export const signIn = () => APIClient.signIn();

/**
 * Signs the user out and resets user data back to empty
 */
export const signOut = (editUser) => {
    localStorage.removeItem(userInfoLocalStorageName);

    editUser((user) => user.merge({
        userInfo: null,
        startedEntityIds: [],
        completedEntityIds: [],
    }));

    return APIClient.signOut();
};

/**
 * Determines if a user is signed in or not
 */
export const isSignedIn = () =>
    APIClient.isSignedIn();

const getLocalStorageName = (base, userInfo) =>
    base + "-uid-" + (userInfo.get("nickname") || userInfo.get("username"));

const completedEntityIdsLocalStorageName = _.partial(getLocalStorageName, "completed");
const startedEntityIdsLocalStorageName = _.partial(getLocalStorageName, "started");
const userVideosLocalStorageName = _.partial(getLocalStorageName, "userVideos");
const userExercisesLocalStorageName = _.partial(getLocalStorageName, "userExercises");

/**
 * Saves a userInfo object (not ImmutableJS)
 */
const saveUserInfo = (userInfo) => {
    localStorage.removeItem(userInfoLocalStorageName);
    localStorage.setItem(userInfoLocalStorageName, JSON.stringify(userInfo));
};

/**
 * Saves a list of started entities (not ImmutableJS)
 */
const saveStarted = (userInfo, startedEntityIds) => {
    localStorage.removeItem(startedEntityIdsLocalStorageName(userInfo));
    localStorage.setItem(startedEntityIdsLocalStorageName(userInfo), JSON.stringify(startedEntityIds));
};

/**
 * Saves a list of completed entities (not ImmutableJS)
 */
const saveCompleted = (userInfo, completedEntityIds) => {
    localStorage.removeItem(completedEntityIdsLocalStorageName(userInfo));
    localStorage.setItem(completedEntityIdsLocalStorageName(userInfo), JSON.stringify(completedEntityIds));
};

/**
 * Saves a list of user entities (not ImmutableJS).
 * THis is mostly used for tracking the last position of the watched video.
 */
const saveUserVideos = (userInfo, userVideos) => {
    userVideos = userVideos.map((userVideo) => {
        return {
            duration: userVideo.duration,
            last_second_watched: userVideo.last_second_watched,
            points: userVideo.points,
            video: {
                id: userVideo.id
            }
        };
    });
    localStorage.removeItem(userVideosLocalStorageName(userInfo));
    localStorage.setItem(userVideosLocalStorageName(userInfo), JSON.stringify(userVideos));
};

/**
 * Saves a list of information on the exercise.
 */
const saveUserExercises = (userInfo, userExercises) => {
    userExercises = userExercises.map((exercise) => {
        return {
            streak: exercise.streak,
            total_correct: exercise.total_correct,
            total_done: exercise.total_done,
            exercise_model: {
                content_id: exercise.exercise_model.content_id
            }
        };
    });

    // The extra removeItem calls before the setItem calls help in case local storage is almost full
    localStorage.removeItem(userExercisesLocalStorageName(userInfo));
    localStorage.setItem(userExercisesLocalStorageName(userInfo), JSON.stringify(userExercises));
};

/**
 * Loads all of the different types of user data from local storage.
 */
const loadLocalStorageData = (userInfo) => {
    var result = {};
    var completedEntityIds = localStorage.getItem(completedEntityIdsLocalStorageName(userInfo));
    if (completedEntityIds) {
        result.completedEntityIds = JSON.parse(completedEntityIds);
    }
    var startedEntityIds = localStorage.getItem(startedEntityIdsLocalStorageName(userInfo));
    if (startedEntityIds) {
        result.startedEntityIds = JSON.parse(startedEntityIds);
    }
    var userVideos = localStorage.getItem(userVideosLocalStorageName(userInfo));
    if (userVideos) {
        result.userVideos = JSON.parse(userVideos);
    }
    var userExercises = localStorage.getItem(userExercisesLocalStorageName(userInfo));
    if (userExercises) {
        result.userExercises = JSON.parse(userExercises);
    }

    return result;
};

/**
 * Reports video progress for the user back to KA
 */
export const reportVideoProgress = (user, topicTreeNode, secondsWatched, lastSecondWatched, editVideo, editUser) => {
    return new Promise((resolve, reject) => {
        const youTubeId = getYoutubeId(topicTreeNode),
            videoId = getId(topicTreeNode),
            duration = getDuration(topicTreeNode),
            editCompletedEntityIds = editorForPath(editUser, "completedEntityIds"),
            editStartedEntityIds = editorForPath(editUser, "startedEntityIds"),
            editUserInfo = editorForPath(editUser, "userInfo"),
            editUserVideos = editorForPath(editUser, "userVideos");
        let completedEntityIds = user.get("completedEntityIds"),
            startedEntityIds = user.get("startedEntityIds"),
            userVideos = user.get("userVideos");

        APIClient.reportVideoProgress(videoId, youTubeId, secondsWatched, lastSecondWatched).then((result) => {
            if (!result) {
                Util.warn("Video progress report returned null results!");
                return;
            }
            Util.log("reportVideoProgress result: %o", result);

            var lastPoints = getPoints(topicTreeNode);
            var newPoints = lastPoints + result.points_earned;
            if (newPoints > 750) {
                newPoints = 750;
            }

            // If they've watched some part of the video, and it's not almost the end
            // Otherwise check if we already have video progress for this item and we
            // therefore no longer need it.
            var lastSecondWatched;
            if (result.last_second_watched > 10 &&
                    duration - result.last_second_watched > 10) {
                lastSecondWatched = result.last_second_watched;
            }

            // If we're just getting a completion of a video update
            // the user's overall points locally.
            if (result.points_earned > 0) {
                var userInfo = user.get("userInfo");
                userInfo = userInfo.set("points",
                    userInfo.get("points") + result.points_earned);
                editUserInfo(() => userInfo);
                saveUserInfo(userInfo);
            }

            editVideo((video) =>
                video.merge({
                    points: newPoints,
                    completed: result.is_video_completed,
                    started: !result.is_video_completed,
                    lastSecondWatched: lastSecondWatched
                }));

            // Update locally stored cached info
            if (result.is_video_completed) {
                if (startedEntityIds.contains(getId(topicTreeNode))) {
                    startedEntityIds = startedEntityIds.remove(startedEntityIds.indexOf(getId(topicTreeNode)));
                    editStartedEntityIds(() => startedEntityIds);
                }
                if (!completedEntityIds.contains(getId(topicTreeNode))) {
                    completedEntityIds = completedEntityIds.unshift(getId(topicTreeNode));
                    editCompletedEntityIds(() => completedEntityIds);
                }
            } else {
                if (!startedEntityIds.contains(getId(topicTreeNode))) {
                    startedEntityIds = startedEntityIds.unshift(getId(topicTreeNode));
                    editStartedEntityIds(() => startedEntityIds);
                }
            }

            var foundUserVideo = userVideos.find((info) =>
                info.getIn(["video", "id"]) === getId(topicTreeNode));
            if (foundUserVideo) {
                foundUserVideo = foundUserVideo.toJS();
            }
            var isNew = !foundUserVideo;
            foundUserVideo = foundUserVideo || {
                video: {
                    id: getId(topicTreeNode)
                },
                duration: getDuration(topicTreeNode)
            };
            foundUserVideo["points"] = newPoints;
            foundUserVideo["last_second_watched"] = lastSecondWatched;
            if (isNew) {
                userVideos = userVideos.unshift(Immutable.fromJS(foundUserVideo));
                editUserVideos(() => userVideos);
            }

            saveStarted(user.get("userInfo"), startedEntityIds);
            saveCompleted(user.get("userInfo"), completedEntityIds);
            saveUserVideos(user.get("userInfo"), userVideos);

            resolve({
                completed: result.is_video_completed,
                lastSecondWatched: result.last_second_watched,
                pointsEarned: result.points_earned,
                youtubeId: result.youtube_id,
                videoId,
                id: getId(topicTreeNode)
            });
        }).catch(() => {
            reject();
        });
    });
};

/**
 * Reports article progress for the user back to KA
 */
export const reportArticleRead = (user, topicTreeNode, editUser) => {
    return new Promise((resolve, reject) => {
        if (!isSignedIn()) {
            return setTimeout(resolve, 0);
        }

        APIClient.reportArticleRead(getId(topicTreeNode)).then((result) => {
            Util.log("reported article complete: %o", result);
            let completedEntityIds = user.get("completedEntityIds");
            if (!completedEntityIds.contains(getId(topicTreeNode))) {
              completedEntityIds = completedEntityIds.unshift(getId(topicTreeNode));
              const editCompletedEntityIds = editorForPath(editUser, "completedEntityIds");
              editCompletedEntityIds(() => completedEntityIds);
            }
            saveCompleted(user.get("userInfo"), completedEntityIds);
            resolve(result);
        }).catch(() => {
            reject();
        });
    });
};

/**
 * Refreshes the logged in info either from local storage or from the server
 */
export const refreshLoggedInInfo = (user, editUser, forceRefreshAllInfo) => {
    return new Promise((resolve, reject) => {
        if (!isSignedIn()) {
            return resolve();
        }

        // Get the user profile info
        APIClient.getUserInfo().then((result) => {
            Util.log("getUserInfo: %o", result);
            const userInfo = Immutable.fromJS({
                avatarUrl: result.avatar_url,
                joined: result.joined,
                nickname: result.nickname,
                username: result.username,
                points: result.points,
                badgeCounts: result.badge_counts
            });

            editUser((user) => user.set("userInfo", userInfo));

            saveUserInfo(userInfo.toJS());

            var result = loadLocalStorageData(userInfo);
            if (!forceRefreshAllInfo && result.completedEntityIds && result.startedEntityIds) {
                Util.log("User info only obtained. Not obtaining user data because we have it cached already!");
                return result;
            }

            // The call is needed for completed/in progress status of content items
            // Unlike getUserVideos, this includes both articles and videos.
            return APIClient.getUserProgress();

        }).then((data) => {
            Util.log("getUserProgress: %o", data);
            // data.complete will be returned from the API but not when loaded
            // directly from localStorage.  When loaded directly from localstorage
            // you'll already have result.startedEntityIds.
            if (data.complete) {
                // Get rid of the 'a' and 'v' prefixes, and set the completed / started
                // attributes accordingly.
                data.startedEntityIds = _.map(data.started, function(e) {
                    return e.substring(1);
                });
                data.completedEntityIds = _.map(data.complete, function(e) {
                    return e.substring(1);
                });

                // Save to local storage
                saveStarted(user.get("userInfo"), data.startedEntityIds);
                saveCompleted(user.get("userInfo"), data.completedEntityIds);
            }

            editUser((user) => user.merge({
                startedEntityIds: data.startedEntityIds,
                completedEntityIds: data.completedEntityIds,
            }));

            return APIClient.getUserVideos();
        }).then((userVideosResults) => {
            saveUserVideos(user.get("userInfo"), userVideosResults);
            return APIClient.getUserExercises();
        }).then((userExercisesResults) => {
            saveUserExercises(user.get("userInfo"), userExercisesResults);
            resolve();
        }).catch(() => {
            reject();
        });
    });
};
<|endoftext|>


<commit_before>"use strict";

var partial  = require("es5-ext/function/#/partial")
  , forEach  = require("es5-ext/object/for-each")
  , pad      = require("es5-ext/string/#/pad")
  , deferred = require("./deferred")

  , resolved, rStats, unresolved, uStats, profile;

exports.profile = function () {
	resolved = 0;
	rStats = {};
	unresolved = 0;
	uStats = {};
	deferred._profile = profile;
};

profile = function (isResolved) {
	var stack, data;

	if (isResolved) {
		++resolved;
		data = rStats;
	} else {
		++unresolved;
		data = uStats;
	}

	stack = (new Error()).stack;
	if (!stack.split("\n").slice(3).some(function (line) {
			if ((line.search(/[\/\\]deferred[\/\\]/) === -1) &&
					(line.search(/[\/\\]es5-ext[\/\\]/) === -1) &&
					(line.indexOf(" (native)") === -1)) {
				line = line.replace(/\n/g, "\\n").trim();
				if (!data[line]) {
					data[line] = { count: 0 };
				}
				++data[line].count;
				return true;
			}
		})) {
		if (!data.unknown) {
			data.unknown = { count: 0, stack: stack };
		}
		++data.unknown.count;
	}
};

exports.profileEnd = function () {
	var total, lpad, log = "";

	if (!deferred._profile) {
		throw new Error("Deferred profiler was not initialized");
	}
	delete deferred._profile;

	log += "------------------------------------------------------------\n";
	log += "Deferred usage statistics:\n\n";

	total = String(resolved + unresolved);
	lpad = partial.call(pad, " ", total.length);
	log += total + " Total promises initialized\n";
	log += lpad.call(unresolved) + " Initialized as Unresolved\n";
	log += lpad.call(resolved) + " Initialized as Resolved\n";

	if (unresolved) {
		log += "\nUnresolved promises were initialized at:\n";
		forEach(uStats, function (data, name) {
			log += lpad.call(data.count) + " " + name + "\n";
		}, null, function (a, b) {
			return this[b].count - this[a].count;
		});
	}

	if (resolved) {
		log += "\nResolved promises were initialized at:\n";
		forEach(rStats, function (data, name) {
			log += lpad.call(data.count) + " " + name + "\n";
		}, null, function (a, b) {
			return this[b].count - this[a].count;
		});
	}
	log += "------------------------------------------------------------\n";

	return {
		log: log,
		resolved: { count: resolved, stats: rStats },
		unresolved: { count: unresolved, stats: uStats }
	};
};
<commit_msg>style: prettier<commit_after>"use strict";

var partial  = require("es5-ext/function/#/partial")
  , forEach  = require("es5-ext/object/for-each")
  , pad      = require("es5-ext/string/#/pad")
  , deferred = require("./deferred");

var resolved, rStats, unresolved, uStats, profile;

exports.profile = function () {
	resolved = 0;
	rStats = {};
	unresolved = 0;
	uStats = {};
	deferred._profile = profile;
};

profile = function (isResolved) {
	var stack, data;

	if (isResolved) {
		++resolved;
		data = rStats;
	} else {
		++unresolved;
		data = uStats;
	}

	stack = new Error().stack;
	if (
		!stack
			.split("\n")
			.slice(3)
			.some(function (line) {
				if (
					line.search(/[\/\\]deferred[\/\\]/) === -1 &&
					line.search(/[\/\\]es5-ext[\/\\]/) === -1 &&
					line.indexOf(" (native)") === -1
				) {
					line = line.replace(/\n/g, "\\n").trim();
					if (!data[line]) {
						data[line] = { count: 0 };
					}
					++data[line].count;
					return true;
				}
			})
	) {
		if (!data.unknown) {
			data.unknown = { count: 0, stack: stack };
		}
		++data.unknown.count;
	}
};

exports.profileEnd = function () {
	var total, lpad, log = "";

	if (!deferred._profile) {
		throw new Error("Deferred profiler was not initialized");
	}
	delete deferred._profile;

	log += "------------------------------------------------------------\n";
	log += "Deferred usage statistics:\n\n";

	total = String(resolved + unresolved);
	lpad = partial.call(pad, " ", total.length);
	log += total + " Total promises initialized\n";
	log += lpad.call(unresolved) + " Initialized as Unresolved\n";
	log += lpad.call(resolved) + " Initialized as Resolved\n";

	if (unresolved) {
		log += "\nUnresolved promises were initialized at:\n";
		forEach(
			uStats,
			function (data, name) {
				log += lpad.call(data.count) + " " + name + "\n";
			},
			null,
			function (a, b) {
				return this[b].count - this[a].count;
			}
		);
	}

	if (resolved) {
		log += "\nResolved promises were initialized at:\n";
		forEach(
			rStats,
			function (data, name) {
				log += lpad.call(data.count) + " " + name + "\n";
			},
			null,
			function (a, b) {
				return this[b].count - this[a].count;
			}
		);
	}
	log += "------------------------------------------------------------\n";

	return {
		log: log,
		resolved: { count: resolved, stats: rStats },
		unresolved: { count: unresolved, stats: uStats }
	};
};
<|endoftext|>


<commit_before>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Params = imports.params;

function loadUI(resourcePath, objects) {
    let ui = new Gtk.Builder();

    if (objects) {
        for (let o in objects)
            ui.expose_object(o, objects[o]);
    }

    ui.add_from_resource(resourcePath);
    return ui;
}

function loadStyleSheet(resource) {
    let provider = new Gtk.CssProvider();
    provider.load_from_file(Gio.File.new_for_uri('resource://' + resource));
    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
                                             provider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}

function initActions(actionMap, simpleActionEntries, context) {
    simpleActionEntries.forEach(function(entry) {
        let filtered = Params.filter(entry, { activate: null,
                                              state_changed: null,
                                              context: null });
        let action = new Gio.SimpleAction(entry);

        let context = filtered.context || actionMap;
        if (filtered.activate)
            action.connect('activate', filtered.activate.bind(context));
        if (filtered.state_changed)
            action.connect('state-changed', filtered.state_changed.bind(context));

        actionMap.add_action(action);
    });
}

function arrayEqual(one, two) {
    if (one.length != two.length)
        return false;

    for (let i = 0; i < one.length; i++)
        if (one[i] != two[i])
            return false;

    return true;
}

function getSettings(schemaId, path) {
    const GioSSS = Gio.SettingsSchemaSource;
    let schemaSource;

    if (!pkg.moduledir.startsWith('resource://')) {
        // Running from the source tree
        schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
                                                 GioSSS.get_default(),
                                                 false);
    } else {
        schemaSource = GioSSS.get_default();
    }

    let schemaObj = schemaSource.lookup(schemaId, true);
    if (!schemaObj) {
        log('Missing GSettings schema ' + schemaId);
        System.exit(1);
    }

    if (path === undefined)
        return new Gio.Settings({ settings_schema: schemaObj });
    else
        return new Gio.Settings({ settings_schema: schemaObj,
                                  path: path });
}

function loadIcon(iconName, size) {
    let theme = Gtk.IconTheme.get_default();

    return theme.load_icon(iconName,
                           size,
                           Gtk.IconLookupFlags.GENERIC_FALLBACK);
}
<commit_msg>Use correct signal name of GSimpleAction::change-state<commit_after>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Params = imports.params;

function loadUI(resourcePath, objects) {
    let ui = new Gtk.Builder();

    if (objects) {
        for (let o in objects)
            ui.expose_object(o, objects[o]);
    }

    ui.add_from_resource(resourcePath);
    return ui;
}

function loadStyleSheet(resource) {
    let provider = new Gtk.CssProvider();
    provider.load_from_file(Gio.File.new_for_uri('resource://' + resource));
    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
                                             provider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}

function initActions(actionMap, simpleActionEntries, context) {
    simpleActionEntries.forEach(function(entry) {
        let filtered = Params.filter(entry, { activate: null,
                                              change_state: null,
                                              context: null });
        let action = new Gio.SimpleAction(entry);

        let context = filtered.context || actionMap;
        if (filtered.activate)
            action.connect('activate', filtered.activate.bind(context));
        if (filtered.change_state)
            action.connect('change-state', filtered.change_state.bind(context));

        actionMap.add_action(action);
    });
}

function arrayEqual(one, two) {
    if (one.length != two.length)
        return false;

    for (let i = 0; i < one.length; i++)
        if (one[i] != two[i])
            return false;

    return true;
}

function getSettings(schemaId, path) {
    const GioSSS = Gio.SettingsSchemaSource;
    let schemaSource;

    if (!pkg.moduledir.startsWith('resource://')) {
        // Running from the source tree
        schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
                                                 GioSSS.get_default(),
                                                 false);
    } else {
        schemaSource = GioSSS.get_default();
    }

    let schemaObj = schemaSource.lookup(schemaId, true);
    if (!schemaObj) {
        log('Missing GSettings schema ' + schemaId);
        System.exit(1);
    }

    if (path === undefined)
        return new Gio.Settings({ settings_schema: schemaObj });
    else
        return new Gio.Settings({ settings_schema: schemaObj,
                                  path: path });
}

function loadIcon(iconName, size) {
    let theme = Gtk.IconTheme.get_default();

    return theme.load_icon(iconName,
                           size,
                           Gtk.IconLookupFlags.GENERIC_FALLBACK);
}
<|endoftext|>


<commit_before>/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

import path from 'path';

/**
 * Promisified version of compiler.runAsChild() with error hoisting and isolated output/assets.
 * (runAsChild() merges assets into the parent compilation, we don't want that)
 */
export function runChildCompiler (compiler) {
  return new Promise((resolve, reject) => {
    compiler.compile((err, compilation) => {
      // still allow the parent compiler to track execution of the child:
      compiler.parentCompilation.children.push(compilation);
      if (err) return reject(err);

      // Bubble stat errors up and reject the Promise:
      if (compilation.errors && compilation.errors.length) {
        const errorDetails = compilation.errors.map(error => error.details).join('\n');
        return reject(Error('Child compilation failed:\n' + errorDetails));
      }

      resolve(compilation);
    });
  });
}

/** Crawl up the compiler tree and return the outermost compiler instance */
export function getRootCompiler (compiler) {
  while (compiler.parentCompilation && compiler.parentCompilation.compiler) {
    compiler = compiler.parentCompilation.compiler;
  }
  return compiler;
}

/** Find the best possible export for an ES Module. Returns `undefined` for no exports. */
export function getBestModuleExport (exports) {
  if (exports.default) {
    return exports.default;
  }
  for (const prop in exports) {
    if (prop !== '__esModule') {
      return exports[prop];
    }
  }
}

/** Wrap a String up into an ES Module that exports it */
export function stringToModule (str) {
  return 'export default ' + JSON.stringify(str);
}

export function convertPathToRelative (context, entry, prefix = '') {
  if (Array.isArray(entry)) {
    return entry.map(entry => prefix + path.relative(context, entry));
  } else if (entry && typeof entry === 'object') {
    return Object.keys(entry).reduce((acc, key) => {
      acc[key] = prefix + path.relative(context, entry[key]);
      return acc;
    }, {});
  }
  return prefix + path.relative(context, entry);
}
<commit_msg>fix: entry chunk is array type<commit_after>/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

import path from 'path';

/**
 * Promisified version of compiler.runAsChild() with error hoisting and isolated output/assets.
 * (runAsChild() merges assets into the parent compilation, we don't want that)
 */
export function runChildCompiler (compiler) {
  return new Promise((resolve, reject) => {
    compiler.compile((err, compilation) => {
      // still allow the parent compiler to track execution of the child:
      compiler.parentCompilation.children.push(compilation);
      if (err) return reject(err);

      // Bubble stat errors up and reject the Promise:
      if (compilation.errors && compilation.errors.length) {
        const errorDetails = compilation.errors.map(error => error.details).join('\n');
        return reject(Error('Child compilation failed:\n' + errorDetails));
      }

      resolve(compilation);
    });
  });
}

/** Crawl up the compiler tree and return the outermost compiler instance */
export function getRootCompiler (compiler) {
  while (compiler.parentCompilation && compiler.parentCompilation.compiler) {
    compiler = compiler.parentCompilation.compiler;
  }
  return compiler;
}

/** Find the best possible export for an ES Module. Returns `undefined` for no exports. */
export function getBestModuleExport (exports) {
  if (exports.default) {
    return exports.default;
  }
  for (const prop in exports) {
    if (prop !== '__esModule') {
      return exports[prop];
    }
  }
}

/** Wrap a String up into an ES Module that exports it */
export function stringToModule (str) {
  return 'export default ' + JSON.stringify(str);
}

export function convertPathToRelative (context, entry, prefix = '') {
  if (Array.isArray(entry)) {
    return entry.map(entry => prefix + path.relative(context, entry));
  } else if (entry && typeof entry === 'object') {
    return Object.keys(entry).reduce((acc, key) => {
      acc[key] = Array.isArray(entry[key])
        ? entry[key].map(item => prefix + path.relative(context, item))
        : prefix + path.relative(context, entry[key]);
      return acc;
    }, {});
  }
  return prefix + path.relative(context, entry);
}
<|endoftext|>


<commit_before>'use strict';

angular.module('arachne.visualizations.directives')
    .directive('con10tNetwork', ['$http', '$q', '$filter', '$location', function ($http, $q, $filter, $location) {
        return {
            restrict: 'E',
            templateUrl: 'app/visualizations/con10t-network.html',
            scope: {
                placeDataPath: '@',
                objectDataPath: '@',
                personDataPath: '@',
                lat: '@',
                lng: '@',
                zoom: '@',
                objectNameSingular: '@',
                objectNamePlural: '@'
            },
            link: function (scope, element, attrs) {
                scope.evaluateOverallDateRange = function(){
                    scope.overallMinDate = new Date(8640000000000000);
                    scope.overallMaxDate = new Date(-8640000000000000);

                    for(var i = 0; i < scope.rawObjectData.length; i++){
                        var current = scope.rawObjectData[i];

                        if(new Date(current['timespanFrom']) < scope.overallMinDate){
                            scope.overallMinDate = new Date(current['timespanFrom'])
                        }

                        if(new Date(current['timespanTo']) > scope.overallMaxDate) {
                            scope.overallMaxDate = new Date(current['timespanTo'])
                        }
                    }

                    scope.minDate = scope.overallMinDate;
                    scope.maxDate = scope.overallMaxDate;
                };

                scope.evaluateActiveObjectData = function() {
                    scope.arachneIds = [];
                    scope.activeObjectCount = 0;

                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        if(scope.rawObjectData[i]['arachneId'] !== 'null') {
                            scope.arachneIds.push(scope.rawObjectData[i]['arachneId']);
                        }
                        scope.activeObjectCount += 1
                    }
                };

                scope.createTimeLineBins = function(){
                    scope.timeDataBins = [];
                    scope.binnedData = {};

                    for (var i = 0; i < scope.rawObjectData.length; i++) {
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        var currentObject = scope.rawObjectData[i];

                        var fromDate = new Date(currentObject['timespanFrom']);
                        var toDate = new Date(currentObject['timespanTo']);
                        if (isNaN(fromDate.getDate()) || isNaN(toDate.getDate())) {
                            continue;
                        }
                        if (fromDate.toISOString() === toDate.toISOString()) {
                            var binKey = fromDate.toISOString().substr(0, 4);

                            if (binKey in scope.binnedData) {
                                scope.binnedData[binKey] += 1
                            } else {
                                scope.binnedData[binKey] = 1
                            }
                        } else {
                            var fromBinKey = fromDate.toISOString().substr(0, 4);
                            var toBinKey = toDate.toISOString().substr(0, 4);

                            if (fromBinKey in scope.binnedData) {
                                scope.binnedData[fromBinKey] += 1
                            } else {
                                scope.binnedData[fromBinKey] = 1
                            }

                            if (toBinKey in scope.binnedData) {
                                scope.binnedData[toBinKey] += 1
                            } else {
                                scope.binnedData[toBinKey] = 1
                            }
                        }
                    }

                    for (var binKey in scope.binnedData) {
                        scope.timeDataBins.push({
                            'date': new Date(binKey),
                            'count': scope.binnedData[binKey]
                        });
                    }

                    scope.timeDataBins.sort(function (a, b) {
                        return a.date - b.date;
                    });

                    var getYearsInbetween = function (startYear, endYear) {
                        var result = [];
                        var currentYear = startYear + 1;

                        while (currentYear < endYear) {
                            result.push(
                                {
                                    'date': new Date(currentYear.toString()),
                                    'count': 0
                                });
                            currentYear += 1;
                        }

                        return result;
                    };

                    // Fill up missing year bins (= years with 0 objects)
                    var inbetween = getYearsInbetween(
                        scope.overallMinDate.getFullYear() - 1,
                        scope.timeDataBins[0]['date'].getFullYear()
                    );

                    scope.timeDataBins = inbetween.concat(scope.timeDataBins);

                    for(var i = 0; i < scope.timeDataBins.length - 1; i++) {

                        if(scope.timeDataBins[i]['date'].getFullYear() + 1
                            !== scope.timeDataBins[i + 1]['date'].getFullYear()){
                            var inbetween = getYearsInbetween(
                                scope.timeDataBins[i]['date'].getFullYear(),
                                scope.timeDataBins[i + 1]['date'].getFullYear()
                            );

                            for(var j = 0; j < inbetween.length; j++){
                                scope.timeDataBins.splice(i + 1 + j, 0, inbetween[j])
                            }
                        }
                    }

                    var inbetween = getYearsInbetween(
                        scope.timeDataBins[scope.timeDataBins.length - 1]['date'].getFullYear(),
                        scope.overallMaxDate.getFullYear() + 1
                    );
                    scope.timeDataBins = scope.timeDataBins.concat(inbetween);
                };

                scope.evaluateVisiblePlaces = function() {
                    scope.visiblePlaces = [];
                    scope.visibleConnections = [];

                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;

                        var currentObject = scope.rawObjectData[i];
                        var alreadyAdded = function (newPlace) {
                            return scope.visiblePlaces.some(function(place){
                                return place['id'] === newPlace['id'];
                            })
                        };

                        if(currentObject['originPlaceId'] !== 'null'){
                            var originPlace = scope.rawPlaceData[
                                scope.placeIndexById[currentObject['originPlaceId']]
                                ];

                            if(!alreadyAdded(originPlace)) {
                                scope.visiblePlaces.push(originPlace);
                            }
                        }

                        if(currentObject['destinationPlaceId'] !== 'null'){
                            var destinationPlace = scope.rawPlaceData[
                                scope.placeIndexById[currentObject['destinationPlaceId']]
                                ];

                            if(!alreadyAdded(destinationPlace)) {
                                scope.visiblePlaces.push(destinationPlace);
                            }
                        }

                        if(currentObject['originPlaceId'] !== 'null' && typeof originPlace !== 'undefined' &&
                            currentObject['destinationPlaceId'] !== 'null' && typeof destinationPlace !== 'undefined'){
                            scope.visibleConnections.push([
                                currentObject['originPlaceId'], currentObject['destinationPlaceId']
                            ]);
                        }
                    }
                };

                scope.evaluateTopPersonConnections = function(){

                    var combineKey = function (authorId, recipientId) {
                        return authorId + ':::' + recipientId;
                    };
                    var splitKey = function (key) {
                        return key.split(':::')
                    };

                    var generateMatrix = function(rows, values) {
                        var result = [];

                        for(var row = 0; row < rows.length; row++){
                            result[row] = [];
                            for(var column = 0; column < rows.length; column++){
                                result[row][column] = 0;
                            }

                            for(var idx = 0; idx < values.length; idx++){
                                if(values[idx][0] === rows[row]){
                                    result[row][rows.indexOf(values[idx][1])] = values[idx][2]
                                }
                            }
                        }
                        return result;
                    };

                    var personConnections = {};
                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        var currentKey = combineKey(
                            scope.rawObjectData[i]['authorId'],
                            scope.rawObjectData[i]['recipientId']
                        );

                        if(currentKey in personConnections) {
                            personConnections[currentKey] += 1
                        } else{
                            personConnections[currentKey] = 1
                        }
                    }

                    var personConnectionsSorted = [];
                    for(var key in personConnections){
                        var split = splitKey(key);
                        var authorId = split[0];
                        var recipientId = split[1];

                        personConnectionsSorted.push([authorId, recipientId, personConnections[key]]);
                    }
                    personConnectionsSorted = personConnectionsSorted.sort(function(x, y) {
                        return y[2] - x[2];
                    }).slice(0, 10);

                    var tempConnections = personConnectionsSorted.slice(0, 10);

                    var topPersonsIds = [];
                    for(var idx in tempConnections){
                        var authorId = tempConnections[idx][0];
                        var recipientId = tempConnections[idx][1];

                        if(topPersonsIds.indexOf(authorId) < 0) {
                            topPersonsIds.push(authorId);
                        }

                        if(topPersonsIds.indexOf(recipientId) < 0){
                            topPersonsIds.push(recipientId);
                        }
                    }

                    var stack1 = [];
                    var stack2 = [];
                    for(var idx in topPersonsIds){
                        if(idx % 2 === 0){
                            stack1.push(topPersonsIds[idx]);
                        } else {
                            stack2.push(topPersonsIds[idx]);
                        }
                    }

                    topPersonsIds = stack1.concat(stack2);

                    var topConnections = [];
                    for(var idx in personConnectionsSorted) {
                        var currentConnection = personConnectionsSorted[idx];
                        if(
                            topPersonsIds.indexOf(currentConnection[0]) >= 0 &&
                            topPersonsIds.indexOf(currentConnection[1]) >= 0
                        ){
                            topConnections.push(currentConnection);
                        }
                    }

                    scope.matrix = generateMatrix(topPersonsIds, topConnections);
                    scope.names = [];

                    for(var i = 0; i < topPersonsIds.length; i++){
                        scope.names.push(scope.rawPersonData[scope.personIndexById[topPersonsIds[i]]]['name'])
                    }
                };

                scope.evaluateState = function(){
                    if(typeof scope.rawPlaceData === 'undefined' || typeof scope.rawObjectData === 'undefined'){
                        return;
                    }
                    scope.evaluateActiveObjectData();
                    scope.createTimeLineBins();
                    scope.evaluateVisiblePlaces();
                    scope.evaluateTopPersonConnections();

                    if(!scope.$root.$$phase && !scope.$$phase) {
                        scope.$apply();
                    }
                };

                scope.isObjectWithinSelectedTimeSpan = function(objectData){
                    if(isNaN(Date.parse(objectData['timespanFrom']))
                        || Date.parse(objectData['timespanFrom']) < scope.minDate
                    ) return false;

                    if(isNaN(Date.parse(objectData['timespanTo']))
                        || Date.parse(objectData['timespanTo']) > scope.maxDate
                    ) return false;

                    return true;
                };

                scope.isObjectIgnoredDueToSelectedPlace = function(objectData){
                    var isIgnored = true;
                    if(scope.selectedPlaceId == null)
                        isIgnored = false;
                    if(objectData['originPlaceId'] !== 'null'
                        && scope.selectedPlaceId === objectData['originPlaceId']) {
                        isIgnored = false;
                    }
                    if(objectData['destinationPlaceId'] !== 'null'
                        && scope.selectedPlaceId === objectData['destinationPlaceId']){
                        isIgnored = false;
                    }
                    return isIgnored;
                };

                // Creates a lookup-index for a list of data objects based on the given key
                scope.createIndex = function (data, indexKey){
                    var index = {};

                    var i = 0;
                    while(i < data.length) {
                        index[data[i][indexKey]] = i;
                        i += 1;
                    }

                    return index;
                };

                scope.loadData = function() {
                    var objectDataColumns = ['id', 'arachneId', 'timespanFrom', 'timespanTo', 'originPlaceId',
                        'destinationPlaceId', 'authorId', 'recipientId'];
                    var placesDataColumns = [ 'id', 'lat', 'lng', 'name', 'authId', 'authSource' ];
                    var personDataColumns = [ 'id', 'authId', 'authSource', 'name'];

                    var dataQueries = [];
                    dataQueries.push($http.get(scope.objectDataPath));
                    dataQueries.push($http.get(scope.placeDataPath));
                    dataQueries.push($http.get(scope.personDataPath));

                    scope.colors = [
                        "#89b7e5",
                        "#6699cc",
                        "#5b89e5",
                        "#3399cc",
                        "#336699",
                        "#75A3D1",
                        "#668899",
                        "#255177",
                        "#0066cc",
                        "#003366"
                    ];

                    $q.all(dataQueries)
                        .then(function (responses) {
                            scope.rawObjectData = $filter('tsvData')(responses[0].data, objectDataColumns);
                            scope.rawPlaceData = $filter('tsvData')(responses[1].data, placesDataColumns);
                            scope.rawPersonData = $filter('tsvData')(responses[2].data, personDataColumns);

                            scope.placeIndexById = scope.createIndex(scope.rawPlaceData, 'id');
                            scope.objectIndexById = scope.createIndex(scope.rawObjectData, 'id');
                            scope.personIndexById = scope.createIndex(scope.rawPersonData, 'id');

                            scope.evaluateOverallDateRange();
                            scope.evaluateState();
                        });
                };

                scope.searchArachneIds = function() {

                    if(scope.arachneIds.length === 0) return;

                    var path = 'search?q=entityId:' + scope.arachneIds[0];
                    for(var i = 1; i < scope.arachneIds.length; i++) {
                        path += ' OR entityId:' + scope.arachneIds[i]
                    }

                    $location.url(path)
                };

                scope.$watch('minDate', function(newValue, oldValue) {
                    scope.evaluateState();
                });

                scope.$watch('maxDate', function(newValue, oldValue) {
                    scope.evaluateState()
                });

                scope.$watch('selectedPlaceId', function(newValue, oldValue){
                    scope.evaluateState();
                });

                scope.selectedPlaceId = null;
                scope.arachneIds = [];
                scope.activeObjectCount = 0;
                scope.loadData();
            }
        }
    }]);
<commit_msg>Add additional year (empty) bin to timeline<commit_after>'use strict';

angular.module('arachne.visualizations.directives')
    .directive('con10tNetwork', ['$http', '$q', '$filter', '$location', function ($http, $q, $filter, $location) {
        return {
            restrict: 'E',
            templateUrl: 'app/visualizations/con10t-network.html',
            scope: {
                placeDataPath: '@',
                objectDataPath: '@',
                personDataPath: '@',
                lat: '@',
                lng: '@',
                zoom: '@',
                objectNameSingular: '@',
                objectNamePlural: '@'
            },
            link: function (scope, element, attrs) {
                scope.evaluateOverallDateRange = function(){
                    scope.overallMinDate = new Date(8640000000000000);
                    scope.overallMaxDate = new Date(-8640000000000000);

                    for(var i = 0; i < scope.rawObjectData.length; i++){
                        var current = scope.rawObjectData[i];

                        if(new Date(current['timespanFrom']) < scope.overallMinDate){
                            scope.overallMinDate = new Date(current['timespanFrom'])
                        }

                        if(new Date(current['timespanTo']) > scope.overallMaxDate) {
                            scope.overallMaxDate = new Date(current['timespanTo'])
                        }
                    }

                    scope.minDate = scope.overallMinDate;
                    scope.maxDate = scope.overallMaxDate;
                };

                scope.evaluateActiveObjectData = function() {
                    scope.arachneIds = [];
                    scope.activeObjectCount = 0;

                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        if(scope.rawObjectData[i]['arachneId'] !== 'null') {
                            scope.arachneIds.push(scope.rawObjectData[i]['arachneId']);
                        }
                        scope.activeObjectCount += 1
                    }
                };

                scope.createTimeLineBins = function(){
                    scope.timeDataBins = [];
                    scope.binnedData = {};

                    for (var i = 0; i < scope.rawObjectData.length; i++) {
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        var currentObject = scope.rawObjectData[i];

                        var fromDate = new Date(currentObject['timespanFrom']);
                        var toDate = new Date(currentObject['timespanTo']);
                        if (isNaN(fromDate.getDate()) || isNaN(toDate.getDate())) {
                            continue;
                        }
                        if (fromDate.toISOString() === toDate.toISOString()) {
                            var binKey = fromDate.toISOString().substr(0, 4);

                            if (binKey in scope.binnedData) {
                                scope.binnedData[binKey] += 1
                            } else {
                                scope.binnedData[binKey] = 1
                            }
                        } else {
                            var fromBinKey = fromDate.toISOString().substr(0, 4);
                            var toBinKey = toDate.toISOString().substr(0, 4);

                            if (fromBinKey in scope.binnedData) {
                                scope.binnedData[fromBinKey] += 1
                            } else {
                                scope.binnedData[fromBinKey] = 1
                            }

                            if (toBinKey in scope.binnedData) {
                                scope.binnedData[toBinKey] += 1
                            } else {
                                scope.binnedData[toBinKey] = 1
                            }
                        }
                    }

                    for (var binKey in scope.binnedData) {
                        scope.timeDataBins.push({
                            'date': new Date(binKey),
                            'count': scope.binnedData[binKey]
                        });
                    }

                    scope.timeDataBins.sort(function (a, b) {
                        return a.date - b.date;
                    });

                    var getYearsInbetween = function (startYear, endYear) {
                        var result = [];
                        var currentYear = startYear + 1;

                        while (currentYear < endYear) {
                            result.push(
                                {
                                    'date': new Date(currentYear.toString()),
                                    'count': 0
                                });
                            currentYear += 1;
                        }

                        return result;
                    };

                    // Fill up missing year bins (= years with 0 objects)
                    var inbetween = getYearsInbetween(
                        scope.overallMinDate.getFullYear() - 1,
                        scope.timeDataBins[0]['date'].getFullYear()
                    );

                    scope.timeDataBins = inbetween.concat(scope.timeDataBins);
                    for(var i = 0; i < scope.timeDataBins.length - 1; i++) {

                        if(scope.timeDataBins[i]['date'].getFullYear() + 1
                            !== scope.timeDataBins[i + 1]['date'].getFullYear()){
                            var inbetween = getYearsInbetween(
                                scope.timeDataBins[i]['date'].getFullYear(),
                                scope.timeDataBins[i + 1]['date'].getFullYear()
                            );

                            for(var j = 0; j < inbetween.length; j++){
                                scope.timeDataBins.splice(i + 1 + j, 0, inbetween[j])
                            }
                        }
                    }
                    var inbetween = getYearsInbetween(
                        scope.timeDataBins[scope.timeDataBins.length - 1]['date'].getFullYear(),
                        scope.overallMaxDate.getFullYear() + 2
                    );
                    scope.timeDataBins = scope.timeDataBins.concat(inbetween);
                };

                scope.evaluateVisiblePlaces = function() {
                    scope.visiblePlaces = [];
                    scope.visibleConnections = [];

                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;

                        var currentObject = scope.rawObjectData[i];
                        var alreadyAdded = function (newPlace) {
                            return scope.visiblePlaces.some(function(place){
                                return place['id'] === newPlace['id'];
                            })
                        };

                        if(currentObject['originPlaceId'] !== 'null'){
                            var originPlace = scope.rawPlaceData[
                                scope.placeIndexById[currentObject['originPlaceId']]
                                ];

                            if(!alreadyAdded(originPlace)) {
                                scope.visiblePlaces.push(originPlace);
                            }
                        }

                        if(currentObject['destinationPlaceId'] !== 'null'){
                            var destinationPlace = scope.rawPlaceData[
                                scope.placeIndexById[currentObject['destinationPlaceId']]
                                ];

                            if(!alreadyAdded(destinationPlace)) {
                                scope.visiblePlaces.push(destinationPlace);
                            }
                        }

                        if(currentObject['originPlaceId'] !== 'null' && typeof originPlace !== 'undefined' &&
                            currentObject['destinationPlaceId'] !== 'null' && typeof destinationPlace !== 'undefined'){
                            scope.visibleConnections.push([
                                currentObject['originPlaceId'], currentObject['destinationPlaceId']
                            ]);
                        }
                    }
                };

                scope.evaluateTopPersonConnections = function(){

                    var combineKey = function (authorId, recipientId) {
                        return authorId + ':::' + recipientId;
                    };
                    var splitKey = function (key) {
                        return key.split(':::')
                    };

                    var generateMatrix = function(rows, values) {
                        var result = [];

                        for(var row = 0; row < rows.length; row++){
                            result[row] = [];
                            for(var column = 0; column < rows.length; column++){
                                result[row][column] = 0;
                            }

                            for(var idx = 0; idx < values.length; idx++){
                                if(values[idx][0] === rows[row]){
                                    result[row][rows.indexOf(values[idx][1])] = values[idx][2]
                                }
                            }
                        }
                        return result;
                    };

                    var personConnections = {};
                    for(var i = 0; i < scope.rawObjectData.length; i++) {
                        if(!scope.isObjectWithinSelectedTimeSpan(scope.rawObjectData[i])) continue;
                        if(scope.isObjectIgnoredDueToSelectedPlace(scope.rawObjectData[i])) continue;

                        var currentKey = combineKey(
                            scope.rawObjectData[i]['authorId'],
                            scope.rawObjectData[i]['recipientId']
                        );

                        if(currentKey in personConnections) {
                            personConnections[currentKey] += 1
                        } else{
                            personConnections[currentKey] = 1
                        }
                    }

                    var personConnectionsSorted = [];
                    for(var key in personConnections){
                        var split = splitKey(key);
                        var authorId = split[0];
                        var recipientId = split[1];

                        personConnectionsSorted.push([authorId, recipientId, personConnections[key]]);
                    }
                    personConnectionsSorted = personConnectionsSorted.sort(function(x, y) {
                        return y[2] - x[2];
                    }).slice(0, 10);

                    var tempConnections = personConnectionsSorted.slice(0, 10);

                    var topPersonsIds = [];
                    for(var idx in tempConnections){
                        var authorId = tempConnections[idx][0];
                        var recipientId = tempConnections[idx][1];

                        if(topPersonsIds.indexOf(authorId) < 0) {
                            topPersonsIds.push(authorId);
                        }

                        if(topPersonsIds.indexOf(recipientId) < 0){
                            topPersonsIds.push(recipientId);
                        }
                    }

                    var stack1 = [];
                    var stack2 = [];
                    for(var idx in topPersonsIds){
                        if(idx % 2 === 0){
                            stack1.push(topPersonsIds[idx]);
                        } else {
                            stack2.push(topPersonsIds[idx]);
                        }
                    }

                    topPersonsIds = stack1.concat(stack2);

                    var topConnections = [];
                    for(var idx in personConnectionsSorted) {
                        var currentConnection = personConnectionsSorted[idx];
                        if(
                            topPersonsIds.indexOf(currentConnection[0]) >= 0 &&
                            topPersonsIds.indexOf(currentConnection[1]) >= 0
                        ){
                            topConnections.push(currentConnection);
                        }
                    }

                    scope.matrix = generateMatrix(topPersonsIds, topConnections);
                    scope.names = [];

                    for(var i = 0; i < topPersonsIds.length; i++){
                        scope.names.push(scope.rawPersonData[scope.personIndexById[topPersonsIds[i]]]['name'])
                    }
                };

                scope.evaluateState = function(){
                    if(typeof scope.rawPlaceData === 'undefined' || typeof scope.rawObjectData === 'undefined'){
                        return;
                    }
                    scope.evaluateActiveObjectData();
                    scope.createTimeLineBins();
                    scope.evaluateVisiblePlaces();
                    scope.evaluateTopPersonConnections();

                    if(!scope.$root.$$phase && !scope.$$phase) {
                        scope.$apply();
                    }
                };

                scope.isObjectWithinSelectedTimeSpan = function(objectData){
                    if(isNaN(Date.parse(objectData['timespanFrom']))
                        || Date.parse(objectData['timespanFrom']) < scope.minDate
                    ) return false;

                    if(isNaN(Date.parse(objectData['timespanTo']))
                        || Date.parse(objectData['timespanTo']) > scope.maxDate
                    ) return false;

                    return true;
                };

                scope.isObjectIgnoredDueToSelectedPlace = function(objectData){
                    var isIgnored = true;
                    if(scope.selectedPlaceId == null)
                        isIgnored = false;
                    if(objectData['originPlaceId'] !== 'null'
                        && scope.selectedPlaceId === objectData['originPlaceId']) {
                        isIgnored = false;
                    }
                    if(objectData['destinationPlaceId'] !== 'null'
                        && scope.selectedPlaceId === objectData['destinationPlaceId']){
                        isIgnored = false;
                    }
                    return isIgnored;
                };

                // Creates a lookup-index for a list of data objects based on the given key
                scope.createIndex = function (data, indexKey){
                    var index = {};

                    var i = 0;
                    while(i < data.length) {
                        index[data[i][indexKey]] = i;
                        i += 1;
                    }

                    return index;
                };

                scope.loadData = function() {
                    var objectDataColumns = ['id', 'arachneId', 'timespanFrom', 'timespanTo', 'originPlaceId',
                        'destinationPlaceId', 'authorId', 'recipientId'];
                    var placesDataColumns = [ 'id', 'lat', 'lng', 'name', 'authId', 'authSource' ];
                    var personDataColumns = [ 'id', 'authId', 'authSource', 'name'];

                    var dataQueries = [];
                    dataQueries.push($http.get(scope.objectDataPath));
                    dataQueries.push($http.get(scope.placeDataPath));
                    dataQueries.push($http.get(scope.personDataPath));

                    scope.colors = [
                        "#89b7e5",
                        "#6699cc",
                        "#5b89e5",
                        "#3399cc",
                        "#336699",
                        "#75A3D1",
                        "#668899",
                        "#255177",
                        "#0066cc",
                        "#003366"
                    ];

                    $q.all(dataQueries)
                        .then(function (responses) {
                            scope.rawObjectData = $filter('tsvData')(responses[0].data, objectDataColumns);
                            scope.rawPlaceData = $filter('tsvData')(responses[1].data, placesDataColumns);
                            scope.rawPersonData = $filter('tsvData')(responses[2].data, personDataColumns);

                            scope.placeIndexById = scope.createIndex(scope.rawPlaceData, 'id');
                            scope.objectIndexById = scope.createIndex(scope.rawObjectData, 'id');
                            scope.personIndexById = scope.createIndex(scope.rawPersonData, 'id');

                            scope.evaluateOverallDateRange();
                            scope.evaluateState();
                        });
                };

                scope.searchArachneIds = function() {

                    if(scope.arachneIds.length === 0) return;

                    var path = 'search?q=entityId:' + scope.arachneIds[0];
                    for(var i = 1; i < scope.arachneIds.length; i++) {
                        path += ' OR entityId:' + scope.arachneIds[i]
                    }

                    $location.url(path)
                };

                scope.$watch('minDate', function(newValue, oldValue) {
                    scope.evaluateState();
                });

                scope.$watch('maxDate', function(newValue, oldValue) {
                    scope.evaluateState()
                });

                scope.$watch('selectedPlaceId', function(newValue, oldValue){
                    scope.evaluateState();
                });

                scope.selectedPlaceId = null;
                scope.arachneIds = [];
                scope.activeObjectCount = 0;
                scope.loadData();
            }
        }
    }]);
<|endoftext|>


<commit_before>/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as controller } from '@ember/controller';
import { scheduleOnce } from '@ember/runloop';
import { task } from 'ember-concurrency';
import intersection from 'lodash.intersection';
import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize';

import EmberObject, { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Searchable from 'nomad-ui/mixins/searchable';
import classic from 'ember-classic-decorator';

export default class OptimizeController extends Controller {
  @controller('optimize/summary') summaryController;

  queryParams = [
    {
      searchTerm: 'search',
    },
    {
      qpType: 'type',
    },
    {
      qpStatus: 'status',
    },
    {
      qpDatacenter: 'dc',
    },
    {
      qpPrefix: 'prefix',
    },
  ];

  constructor() {
    super(...arguments);

    this.summarySearch = RecommendationSummarySearch.create({
      dataSource: this,
    });
  }

  @tracked searchTerm = '';

  @tracked qpType = '';
  @tracked qpStatus = '';
  @tracked qpDatacenter = '';
  @tracked qpPrefix = '';

  @selection('qpType') selectionType;
  @selection('qpStatus') selectionStatus;
  @selection('qpDatacenter') selectionDatacenter;
  @selection('qpPrefix') selectionPrefix;

  optionsType = [
    { key: 'service', label: 'Service' },
    { key: 'system', label: 'System' },
  ];

  optionsStatus = [
    { key: 'pending', label: 'Pending' },
    { key: 'running', label: 'Running' },
    { key: 'dead', label: 'Dead' },
  ];

  get optionsDatacenter() {
    const flatten = (acc, val) => acc.concat(val);
    const allDatacenters = new Set(this.model.mapBy('job.datacenters').reduce(flatten, []));

    // Remove any invalid datacenters from the query param/selection
    const availableDatacenters = Array.from(allDatacenters).compact();
    scheduleOnce('actions', () => {
      // eslint-disable-next-line ember/no-side-effects
      this.qpDatacenter = serialize(intersection(availableDatacenters, this.selectionDatacenter));
    });

    return availableDatacenters.sort().map(dc => ({ key: dc, label: dc }));
  }

  get optionsPrefix() {
    // A prefix is defined as the start of a job name up to the first - or .
    // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds
    const hasPrefix = /.[-._]/;

    // Collect and count all the prefixes
    const allNames = this.model.mapBy('job.name');
    const nameHistogram = allNames.reduce((hist, name) => {
      if (hasPrefix.test(name)) {
        const prefix = name.match(/(.+?)[-._]/)[1];
        hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1;
      }
      return hist;
    }, {});

    // Convert to an array
    const nameTable = Object.keys(nameHistogram).map(key => ({
      prefix: key,
      count: nameHistogram[key],
    }));

    // Only consider prefixes that match more than one name
    const prefixes = nameTable.filter(name => name.count > 1);

    // Remove any invalid prefixes from the query param/selection
    const availablePrefixes = prefixes.mapBy('prefix');
    scheduleOnce('actions', () => {
      // eslint-disable-next-line ember/no-side-effects
      this.qpPrefix = serialize(intersection(availablePrefixes, this.selectionPrefix));
    });

    // Sort, format, and include the count in the label
    return prefixes.sortBy('prefix').map(name => ({
      key: name.prefix,
      label: `${name.prefix} (${name.count})`,
    }));
  }

  get filteredSummaries() {
    const {
      selectionType: types,
      selectionStatus: statuses,
      selectionDatacenter: datacenters,
      selectionPrefix: prefixes,
    } = this;

    // A summary’s job must match ALL filter facets, but it can match ANY selection within a facet
    // Always return early to prevent unnecessary facet predicates.
    return this.summarySearch.listSearched.filter(summary => {
      const job = summary.get('job');

      if (types.length && !types.includes(job.get('displayType'))) {
        return false;
      }

      if (statuses.length && !statuses.includes(job.get('status'))) {
        return false;
      }

      if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) {
        return false;
      }

      const name = job.get('name');
      if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) {
        return false;
      }

      return true;
    });
  }

  get activeRecommendationSummary() {
    return this.summaryController.model;
  }

  // This is a task because the accordion uses timeouts for animation
  // eslint-disable-next-line require-yield
  @(task(function*() {
    const currentSummaryIndex = this.filteredSummaries.indexOf(this.activeRecommendationSummary);
    const nextSummary = this.filteredSummaries.objectAt(currentSummaryIndex + 1);

    if (nextSummary) {
      this.transitionToSummary(nextSummary);
    } else {
      this.send('reachedEnd');
    }
  }).drop())
  proceed;

  @action
  transitionToSummary(summary) {
    this.transitionToRoute('optimize.summary', summary.slug, {
      queryParams: { jobNamespace: summary.jobNamespace },
    });
  }

  @action
  setFacetQueryParam(queryParam, selection) {
    this[queryParam] = serialize(selection);
    this.ensureActiveSummaryIsNotExcluded();
  }

  @action
  transitionToFirstSummary() {
    const firstFilteredSummary = this.filteredSummaries.objectAt(0);

    if (firstFilteredSummary) {
      this.transitionToSummary(firstFilteredSummary);
    }
  }

  @action
  ensureActiveSummaryIsNotExcluded() {
    scheduleOnce('actions', () => {
      if (!this.filteredSummaries.includes(this.activeRecommendationSummary)) {
        const firstFilteredSummary = this.filteredSummaries.objectAt(0);

        if (firstFilteredSummary) {
          this.transitionToSummary(firstFilteredSummary);
        } else {
          this.transitionToRoute('optimize');
        }
      }
    });
  }
}

@classic
class RecommendationSummarySearch extends EmberObject.extend(Searchable) {
  @computed
  get fuzzySearchProps() {
    return ['slug'];
  }

  @alias('dataSource.model') listToSearch;
  @alias('dataSource.searchTerm') searchTerm;

  exactMatchEnabled = false;
  fuzzySearchEnabled = true;
  includeFuzzySearchMatches = true;
}
<commit_msg>Remove unused action<commit_after>/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as controller } from '@ember/controller';
import { scheduleOnce } from '@ember/runloop';
import { task } from 'ember-concurrency';
import intersection from 'lodash.intersection';
import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize';

import EmberObject, { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Searchable from 'nomad-ui/mixins/searchable';
import classic from 'ember-classic-decorator';

export default class OptimizeController extends Controller {
  @controller('optimize/summary') summaryController;

  queryParams = [
    {
      searchTerm: 'search',
    },
    {
      qpType: 'type',
    },
    {
      qpStatus: 'status',
    },
    {
      qpDatacenter: 'dc',
    },
    {
      qpPrefix: 'prefix',
    },
  ];

  constructor() {
    super(...arguments);

    this.summarySearch = RecommendationSummarySearch.create({
      dataSource: this,
    });
  }

  @tracked searchTerm = '';

  @tracked qpType = '';
  @tracked qpStatus = '';
  @tracked qpDatacenter = '';
  @tracked qpPrefix = '';

  @selection('qpType') selectionType;
  @selection('qpStatus') selectionStatus;
  @selection('qpDatacenter') selectionDatacenter;
  @selection('qpPrefix') selectionPrefix;

  optionsType = [
    { key: 'service', label: 'Service' },
    { key: 'system', label: 'System' },
  ];

  optionsStatus = [
    { key: 'pending', label: 'Pending' },
    { key: 'running', label: 'Running' },
    { key: 'dead', label: 'Dead' },
  ];

  get optionsDatacenter() {
    const flatten = (acc, val) => acc.concat(val);
    const allDatacenters = new Set(this.model.mapBy('job.datacenters').reduce(flatten, []));

    // Remove any invalid datacenters from the query param/selection
    const availableDatacenters = Array.from(allDatacenters).compact();
    scheduleOnce('actions', () => {
      // eslint-disable-next-line ember/no-side-effects
      this.qpDatacenter = serialize(intersection(availableDatacenters, this.selectionDatacenter));
    });

    return availableDatacenters.sort().map(dc => ({ key: dc, label: dc }));
  }

  get optionsPrefix() {
    // A prefix is defined as the start of a job name up to the first - or .
    // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds
    const hasPrefix = /.[-._]/;

    // Collect and count all the prefixes
    const allNames = this.model.mapBy('job.name');
    const nameHistogram = allNames.reduce((hist, name) => {
      if (hasPrefix.test(name)) {
        const prefix = name.match(/(.+?)[-._]/)[1];
        hist[prefix] = hist[prefix] ? hist[prefix] + 1 : 1;
      }
      return hist;
    }, {});

    // Convert to an array
    const nameTable = Object.keys(nameHistogram).map(key => ({
      prefix: key,
      count: nameHistogram[key],
    }));

    // Only consider prefixes that match more than one name
    const prefixes = nameTable.filter(name => name.count > 1);

    // Remove any invalid prefixes from the query param/selection
    const availablePrefixes = prefixes.mapBy('prefix');
    scheduleOnce('actions', () => {
      // eslint-disable-next-line ember/no-side-effects
      this.qpPrefix = serialize(intersection(availablePrefixes, this.selectionPrefix));
    });

    // Sort, format, and include the count in the label
    return prefixes.sortBy('prefix').map(name => ({
      key: name.prefix,
      label: `${name.prefix} (${name.count})`,
    }));
  }

  get filteredSummaries() {
    const {
      selectionType: types,
      selectionStatus: statuses,
      selectionDatacenter: datacenters,
      selectionPrefix: prefixes,
    } = this;

    // A summary’s job must match ALL filter facets, but it can match ANY selection within a facet
    // Always return early to prevent unnecessary facet predicates.
    return this.summarySearch.listSearched.filter(summary => {
      const job = summary.get('job');

      if (types.length && !types.includes(job.get('displayType'))) {
        return false;
      }

      if (statuses.length && !statuses.includes(job.get('status'))) {
        return false;
      }

      if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) {
        return false;
      }

      const name = job.get('name');
      if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) {
        return false;
      }

      return true;
    });
  }

  get activeRecommendationSummary() {
    return this.summaryController.model;
  }

  // This is a task because the accordion uses timeouts for animation
  // eslint-disable-next-line require-yield
  @(task(function*() {
    const currentSummaryIndex = this.filteredSummaries.indexOf(this.activeRecommendationSummary);
    const nextSummary = this.filteredSummaries.objectAt(currentSummaryIndex + 1);

    if (nextSummary) {
      this.transitionToSummary(nextSummary);
    } else {
      this.send('reachedEnd');
    }
  }).drop())
  proceed;

  @action
  transitionToSummary(summary) {
    this.transitionToRoute('optimize.summary', summary.slug, {
      queryParams: { jobNamespace: summary.jobNamespace },
    });
  }

  @action
  setFacetQueryParam(queryParam, selection) {
    this[queryParam] = serialize(selection);
    this.ensureActiveSummaryIsNotExcluded();
  }

  @action
  ensureActiveSummaryIsNotExcluded() {
    scheduleOnce('actions', () => {
      if (!this.filteredSummaries.includes(this.activeRecommendationSummary)) {
        const firstFilteredSummary = this.filteredSummaries.objectAt(0);

        if (firstFilteredSummary) {
          this.transitionToSummary(firstFilteredSummary);
        } else {
          this.transitionToRoute('optimize');
        }
      }
    });
  }
}

@classic
class RecommendationSummarySearch extends EmberObject.extend(Searchable) {
  @computed
  get fuzzySearchProps() {
    return ['slug'];
  }

  @alias('dataSource.model') listToSearch;
  @alias('dataSource.searchTerm') searchTerm;

  exactMatchEnabled = false;
  fuzzySearchEnabled = true;
  includeFuzzySearchMatches = true;
}
<|endoftext|>