@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
317 lines • 15.2 kB
JavaScript
;
/*
* Copyright © 2019 Atomist, Inc.
*
* 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.
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const automation_client_1 = require("@atomist/automation-client");
const sdm_pack_fingerprint_1 = require("@atomist/sdm-pack-fingerprint");
const bodyParser = require("body-parser");
const _ = require("lodash");
const path = require("path");
const serveStatic = require("serve-static");
const repository_1 = require("../../../views/repository");
const sunburstPage_1 = require("../../../views/sunburstPage");
const topLevelPage_1 = require("../../../views/topLevelPage");
const analysisTrackingRoutes_1 = require("../../analysis/tracking/analysisTrackingRoutes");
const DefaultAspectRegistry_1 = require("../../aspect/DefaultAspectRegistry");
const customReporters_1 = require("../../customize/customReporters");
const treeUtils_1 = require("../../tree/treeUtils");
const api_1 = require("../api");
const overviewPage_1 = require("./overviewPage");
const repositoryListPage_1 = require("./repositoryListPage");
/**
* Add the org page route to Atomist SDM Express server.
* @return {ExpressCustomizer}
*/
function addWebAppRoutes(aspectRegistry, store, analysisTracking, httpClientFactory, instanceMetadata) {
const topLevelRoute = "/overview";
return {
routesToSuggestOnStartup: [{ title: "Atomist Visualizations", route: topLevelRoute }],
customizer: (express, ...handlers) => {
express.use(bodyParser.json()); // to support JSON-encoded bodies
express.use(bodyParser.urlencoded({
extended: true,
}));
express.use(serveStatic(path.join(__dirname, "..", "..", "..", "public"), { index: false }));
express.use(serveStatic(path.join(__dirname, "..", "..", "..", "dist"), { index: false }));
/* redirect / to the org page. This way we can go right here
* for now, and later make a higher-level page if we want.
*/
express.get("/", ...handlers, (req, res) => {
res.redirect(topLevelRoute);
});
const conf = { express, handlers, aspectRegistry, store, instanceMetadata, httpClientFactory, analysisTracking };
exposeDriftPage(conf);
overviewPage_1.exposeOverviewPage(conf, topLevelRoute);
repositoryListPage_1.exposeRepositoryListPage(conf);
exposeRepositoryPage(conf);
exposeExplorePage(conf);
exposeFingerprintReportPage(conf);
exposeCustomReportPage(conf);
analysisTrackingRoutes_1.exposeAnalysisTrackingPage(conf);
},
};
}
exports.addWebAppRoutes = addWebAppRoutes;
function exposeRepositoryPage(conf) {
conf.express.get("/repository", ...conf.handlers, (req, res, next) => __awaiter(this, void 0, void 0, function* () {
try {
const workspaceId = req.query.workspaceId || "*";
const id = req.query.id;
const path = req.query.path || "";
const category = req.query.category || "*";
const analysisResult = yield conf.store.loadById(id, true);
if (!analysisResult) {
res.send(`No project at ${JSON.stringify(id)}`);
return;
}
const everyFingerprint = yield conf.store.fingerprintsForProject(id);
const virtualPaths = _.uniq(everyFingerprint.map(f => f.path)).filter(p => !!p);
const allFingerprints = everyFingerprint.filter(fp => fp.path === path);
// TODO this is nasty. why query deep in the first place?
analysisResult.analysis.fingerprints = allFingerprints;
const mostRecentTimestamp = allFingerprints.length > 0 ? new Date(Math.max(...allFingerprints.map(fp => fp.timestamp.getTime()))) : undefined;
const aspectsAndFingerprints = yield projectFingerprints(conf.aspectRegistry, allFingerprints);
// assign style based on ideal
const ffd = aspectsAndFingerprints.map(aspectAndFingerprints => (Object.assign(Object.assign({}, aspectAndFingerprints), { fingerprints: aspectAndFingerprints.fingerprints.map(fp => (Object.assign(Object.assign({}, fp), { idealDisplayString: displayIdeal(fp, aspectAndFingerprints.aspect), style: displayStyleAccordingToIdeal(fp) }))) })));
const repo = (yield conf.aspectRegistry.tagAndScoreRepos(workspaceId, [analysisResult], { category }))[0];
// TODO nasty
repo.analysis.id.path = path;
res.send(topLevelPage_1.renderStaticReactNode(repository_1.RepoExplorer({
repo,
aspects: _.sortBy(ffd.filter(f => !!f.aspect.displayName), f => f.aspect.displayName),
category,
timestamp: mostRecentTimestamp,
virtualPaths,
}), `Repository Insights`, conf.instanceMetadata));
return;
}
catch (e) {
automation_client_1.logger.error(e);
next(e);
}
}));
}
function exposeExplorePage(conf) {
conf.express.get("/explore", ...conf.handlers, (req, res, next) => {
const tags = req.query.tags || "";
const workspaceId = req.query.workspaceId || "*";
const dataUrl = `/api/v1/${workspaceId}/explore?tags=${tags}`;
const readable = api_1.describeSelectedTagsToAnimals(tags.split(","));
return renderDataUrl(conf.instanceMetadata, workspaceId, {
dataUrl,
heading: "Explore repositories by tag",
title: `Repositories matching ${readable}`,
}, conf.aspectRegistry, conf.httpClientFactory, req, res).catch(next);
});
}
function exposeDriftPage(conf) {
conf.express.get("/drift", ...conf.handlers, (req, res, next) => {
const workspaceId = req.query.workspaceId || "*";
const percentile = req.query.percentile || 0;
const type = req.query.type;
const dataUrl = `/api/v1/${workspaceId}/drift` +
`?percentile=${percentile}` +
(!!type ? `&type=${type}` : "");
return renderDataUrl(conf.instanceMetadata, workspaceId, {
dataUrl,
title: "Drift by aspect",
heading: type ?
`Drift across aspect ${type} with entropy above ${percentile}th percentile` :
`Drift across all aspects with entropy above ${percentile}th percentile`,
subheading: "Sizing shows degree of entropy",
}, conf.aspectRegistry, conf.httpClientFactory, req, res).catch(next);
});
}
function exposeFingerprintReportPage(conf) {
conf.express.get("/fingerprint/:type/:name", ...conf.handlers, (req, res, next) => {
const type = req.params.type;
const name = req.params.name;
const otherLabel = req.query.otherLabel;
const aspect = conf.aspectRegistry.aspectOf(type);
if (!aspect) {
res.status(400).send("No aspect found for type " + type);
return;
}
const fingerprintDisplayName = DefaultAspectRegistry_1.defaultedToDisplayableFingerprintName(aspect)(name);
const workspaceId = req.query.workspaceId || "*";
let dataUrl = `/api/v1/${workspaceId}/fingerprint/${encodeURIComponent(type)}/${encodeURIComponent(name)}?byOrg=${req.query.byOrg === "true"}&progress=${req.query.progress === "true"}&trim=${req.query.trim === "true"}`;
if (otherLabel) {
dataUrl += `&otherLabel=${otherLabel}`;
}
renderDataUrl(conf.instanceMetadata, workspaceId, {
dataUrl,
title: `Atomist aspect drift`,
heading: aspect.displayName,
subheading: fingerprintDisplayName,
}, conf.aspectRegistry, conf.httpClientFactory, req, res).catch(next);
});
}
function exposeCustomReportPage(conf) {
conf.express.get("/report/:name", ...conf.handlers, (req, res, next) => {
const name = req.params.name;
const workspaceId = req.query.workspaceId || "*";
const queryString = jsonToQueryString(req.query);
const dataUrl = `/api/v1/${workspaceId}/report/${name}?${queryString}`;
const reporter = customReporters_1.CustomReporters[name];
if (!reporter) {
throw new Error(`No report named ${name}`);
}
return renderDataUrl(conf.instanceMetadata, workspaceId, {
dataUrl,
heading: name,
title: reporter.summary,
}, conf.aspectRegistry, conf.httpClientFactory, req, res).catch(next);
});
}
// TODO fix any
function renderDataUrl(instanceMetadata, workspaceId, page, aspectRegistry, httpClientFactory, req, res) {
return __awaiter(this, void 0, void 0, function* () {
let tree;
const possibleIdealsForDisplay = [];
const fullUrl = `http://${req.get("host")}${page.dataUrl}`;
try {
const result = yield httpClientFactory.create().exchange(fullUrl, { retry: { retries: 0 } });
tree = result.body;
automation_client_1.logger.info("From %s, got %s", fullUrl, tree.circles.map(c => c.meaning));
}
catch (e) {
throw new Error(`Failure fetching sunburst data from ${fullUrl}: ` + e.message);
}
populateLocalURLs(tree);
automation_client_1.logger.info("Data url=%s", page.dataUrl);
const fieldsToDisplay = ["entropy", "variants", "count"];
res.send(topLevelPage_1.renderStaticReactNode(sunburstPage_1.SunburstPage({
workspaceId,
heading: page.heading,
subheading: page.subheading,
currentIdeal: yield lookForIdealDisplay(aspectRegistry, req.query.type, req.query.name),
possibleIdeals: possibleIdealsForDisplay,
query: req.params.query,
dataUrl: fullUrl,
tree,
selectedTags: req.query.tags ? req.query.tags.split(",") : [],
fieldsToDisplay,
}), page.title, instanceMetadata, [
"/sunburstScript-bundle.js",
]));
});
}
function populateLocalURLs(plantedTree) {
treeUtils_1.visit(plantedTree.tree, (n, level) => {
const circle = plantedTree.circles[level];
if (!circle) {
return true;
}
const d = n;
if (circle && circle.meaning === "aspect name") {
if (d.type) {
d.url = `/fingerprint/${encodeURIComponent(d.type)}/*`;
}
}
if (d.fingerprint_name && d.type) {
d.url = `/fingerprint/${encodeURIComponent(d.type)}/${encodeURIComponent(d.fingerprint_name)}`;
}
return true;
});
}
exports.populateLocalURLs = populateLocalURLs;
function jsonToQueryString(json) {
return Object.keys(json).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(json[key])).join("&");
}
exports.jsonToQueryString = jsonToQueryString;
function displayIdeal(fingerprint, aspect) {
if (idealIsDifferentFromActual(fingerprint)) {
return DefaultAspectRegistry_1.defaultedToDisplayableFingerprint(aspect)(fingerprint.ideal.ideal);
}
if (idealIsElimination(fingerprint)) {
return "eliminate";
}
return "";
}
function lookForIdealDisplay(aspectRegistry, aspectType, fingerprintName) {
return __awaiter(this, void 0, void 0, function* () {
if (!aspectType) {
return undefined;
}
const aspect = aspectRegistry.aspectOf(aspectType);
if (!aspect) {
return undefined;
}
const ideal = yield aspectRegistry.idealStore
.loadIdeal("local", aspectType, fingerprintName);
if (!ideal) {
return undefined;
}
if (!sdm_pack_fingerprint_1.isConcreteIdeal(ideal)) {
return { displayValue: "eliminate" };
}
return { displayValue: DefaultAspectRegistry_1.defaultedToDisplayableFingerprint(aspect)(ideal.ideal) };
});
}
function idealIsElimination(fingerprint) {
return fingerprint.ideal && !sdm_pack_fingerprint_1.isConcreteIdeal(fingerprint.ideal);
}
function idealIsDifferentFromActual(fingerprint) {
return fingerprint.ideal && sdm_pack_fingerprint_1.isConcreteIdeal(fingerprint.ideal) && fingerprint.ideal.ideal.sha !== fingerprint.sha;
}
function idealIsSameAsActual(fingerprint) {
return fingerprint.ideal && sdm_pack_fingerprint_1.isConcreteIdeal(fingerprint.ideal) && fingerprint.ideal.ideal.sha === fingerprint.sha;
}
function displayStyleAccordingToIdeal(fingerprint) {
const redStyle = { color: "red" };
const greenStyle = { color: "green" };
if (idealIsSameAsActual(fingerprint)) {
return greenStyle;
}
if (idealIsDifferentFromActual(fingerprint)) {
return redStyle;
}
if (idealIsElimination(fingerprint)) {
return redStyle;
}
return {};
}
function projectFingerprints(fm, allFingerprintsInOneProject) {
return __awaiter(this, void 0, void 0, function* () {
const result = [];
for (const aspect of fm.aspects) {
const originalFingerprints = _.sortBy(allFingerprintsInOneProject.filter(fp => aspect.name === (fp.type || fp.name)), fp => fp.name);
if (originalFingerprints.length > 0) {
const fingerprints = [];
for (const fp of originalFingerprints) {
fingerprints.push(Object.assign(Object.assign({}, fp), {
// ideal: await this.opts.idealResolver(fp.name),
displayValue: DefaultAspectRegistry_1.defaultedToDisplayableFingerprint(aspect)(fp), displayName: DefaultAspectRegistry_1.defaultedToDisplayableFingerprintName(aspect)(fp.name) }));
}
result.push({
aspect,
fingerprints,
});
}
}
return result;
});
}
//# sourceMappingURL=webAppRoutes.js.map