UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

317 lines 15.2 kB
"use strict"; /* * 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