├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── mstile-150x150.png ├── android-chrome-192x192.png ├── browserconfig.xml ├── site.webmanifest ├── index.html └── 404.html ├── src ├── assets │ ├── gcp.png │ ├── Docs_2020.png │ ├── Gmail_2020.png │ ├── Sheets_2020.png │ ├── Slides_2020.png │ ├── appsscript.png │ ├── GoogleDrive_2020.png │ └── GoogleCalendar_2020.png ├── plugins │ └── vuetify.js ├── js │ ├── store.js │ ├── classes │ │ ├── GitShax.js │ │ ├── GasManifests.js │ │ ├── GitRepo.js │ │ ├── GitFile.js │ │ ├── GitOwner.js │ │ ├── GasManifest.js │ │ ├── tabvisibility.js │ │ └── GitData.js │ ├── storemaps.js │ ├── compress.js │ ├── settings.js │ ├── scrapi.js │ ├── fiddly.js │ ├── gasvizzy.js │ ├── forager.js │ ├── params.js │ ├── cache.js │ ├── gasser.js │ ├── auth.js │ ├── filtering.js │ ├── d3prep.js │ └── storeinitial.js ├── components │ ├── manifestparentcard.vue │ ├── deeper.vue │ ├── repochip.vue │ ├── statscard.vue │ ├── timezonecard.vue │ ├── runtimeversioncard.vue │ ├── repoinfochip.vue │ ├── loginchip.vue │ ├── webappcard.vue │ ├── oauthscopecard.vue │ ├── advancedservicecard.vue │ ├── errorplace.vue │ ├── webappfilter.vue │ ├── librarycard.vue │ ├── libraryfilter.vue │ ├── timezonefilter.vue │ ├── datastudiofilter.vue │ ├── repofilter.vue │ ├── runtimeversionfilter.vue │ ├── advancedservicefilter.vue │ ├── addonfilter.vue │ ├── datastudiocard.vue │ ├── oauthscopefilter.vue │ ├── ownerfilter.vue │ ├── picker.vue │ ├── tagitem.vue │ ├── repocard.vue │ ├── addoncard.vue │ ├── repotree copy.vue │ ├── ownercard.vue │ ├── repotree.vue │ ├── pulldialog.vue │ ├── filecard.vue │ └── icons.vue ├── main.js └── App.vue ├── vue.config.js ├── babel.config.js ├── shots ├── 2021-01-26-11-26-29.png ├── 2021-01-26-11-28-26.png ├── 2021-01-26-11-29-54.png └── 2021-01-26-11-30-40.png ├── .gitignore ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/gcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/gcp.png -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transpileDependencies": [ 3 | "vuetify" 4 | ] 5 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/Docs_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/Docs_2020.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/Gmail_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/Gmail_2020.png -------------------------------------------------------------------------------- /src/assets/Sheets_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/Sheets_2020.png -------------------------------------------------------------------------------- /src/assets/Slides_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/Slides_2020.png -------------------------------------------------------------------------------- /src/assets/appsscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/appsscript.png -------------------------------------------------------------------------------- /shots/2021-01-26-11-26-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/shots/2021-01-26-11-26-29.png -------------------------------------------------------------------------------- /shots/2021-01-26-11-28-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/shots/2021-01-26-11-28-26.png -------------------------------------------------------------------------------- /shots/2021-01-26-11-29-54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/shots/2021-01-26-11-29-54.png -------------------------------------------------------------------------------- /shots/2021-01-26-11-30-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/shots/2021-01-26-11-30-40.png -------------------------------------------------------------------------------- /src/assets/GoogleDrive_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/GoogleDrive_2020.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/GoogleCalendar_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brucemcpherson/gitvizzy/HEAD/src/assets/GoogleCalendar_2020.png -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /src/js/store.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex"; 2 | import Vue from 'vue'; 3 | import storeInitial from './storeinitial'; 4 | 5 | Vue.use(Vuex); 6 | export default new Vuex.Store(storeInitial); 7 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /src/js/classes/GitShax.js: -------------------------------------------------------------------------------- 1 | class GitShax { 2 | 3 | constructor(data) { 4 | if (data.importFields) { 5 | this.fields = data.importFields; 6 | } else { 7 | this.fields = [].reduce((p, c) => { 8 | p[c] = data[c]; 9 | return p; 10 | }, {}); 11 | this.fields.id = data.sha; 12 | } 13 | } 14 | 15 | } 16 | module.exports = GitShax; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | .firebaserc 5 | firebase.json 6 | .firebase 7 | secrets 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /src/components/manifestparentcard.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | 29 | -------------------------------------------------------------------------------- /src/js/storemaps.js: -------------------------------------------------------------------------------- 1 | // a quick wap of mapping all known mutaions, state, mappers and actions 2 | import storeIntial from "./storeinitial"; 3 | import Vuex from 'vuex'; 4 | 5 | const _vxMaps = Object.keys(storeIntial).reduce((p, c) => { 6 | p[c] = Object.keys(storeIntial[c]); 7 | return p; 8 | }, {}); 9 | 10 | const maps = Object.keys(_vxMaps).reduce((p, c) => { 11 | const method = `map${c.slice(0, 1).toUpperCase()}${c.slice(1)}`; 12 | p[c] = Vuex[method](_vxMaps[c]); 13 | return p; 14 | }, {}); 15 | 16 | export default maps; 17 | -------------------------------------------------------------------------------- /src/components/deeper.vue: -------------------------------------------------------------------------------- 1 | 9 | 21 | -------------------------------------------------------------------------------- /src/js/classes/GasManifests.js: -------------------------------------------------------------------------------- 1 | const GasManifest = require('./GasManifest') 2 | 3 | class GasManifests { 4 | constructor({ shaxs }) { 5 | this.manifests = new Map( 6 | Array.from(shaxs, ([key, shax]) => [key, new GasManifest(shax)]) 7 | ); 8 | this._maps = null 9 | } 10 | get maps() { 11 | return this._maps 12 | } 13 | set maps(value) { 14 | this._maps = value 15 | } 16 | labels (type) { 17 | return Array.from(this.maps[type]).map(([id,value]) => ({ 18 | id, 19 | label: value.label 20 | })) 21 | } 22 | } 23 | module.exports = GasManifests -------------------------------------------------------------------------------- /src/components/repochip.vue: -------------------------------------------------------------------------------- 1 | 13 | 26 | -------------------------------------------------------------------------------- /src/components/statscard.vue: -------------------------------------------------------------------------------- 1 | 10 | 31 | -------------------------------------------------------------------------------- /src/js/classes/GitRepo.js: -------------------------------------------------------------------------------- 1 | class GitRepo { 2 | 3 | static decorations = []; 4 | 5 | constructor({ repository, importFields }) { 6 | if (importFields) { 7 | this.fields = importFields; 8 | } else { 9 | const { owner } = repository; 10 | this.fields = ["id", "full_name", "name", "html_url", "url"].reduce( 11 | (p, c) => { 12 | p[c] = repository[c]; 13 | return p; 14 | }, 15 | {} 16 | ); 17 | this.fields.ownerId = owner.id; 18 | 19 | } 20 | } 21 | 22 | 23 | decorate(body) { 24 | if (body) { 25 | this.constructor.decorations.forEach((f) => { 26 | this.fields[f] = body[f]; 27 | }); 28 | } 29 | } 30 | } 31 | module.exports = GitRepo; 32 | -------------------------------------------------------------------------------- /src/components/timezonecard.vue: -------------------------------------------------------------------------------- 1 | 18 | 31 | 47 | -------------------------------------------------------------------------------- /src/components/runtimeversioncard.vue: -------------------------------------------------------------------------------- 1 | 18 | 31 | 47 | -------------------------------------------------------------------------------- /src/js/classes/GitFile.js: -------------------------------------------------------------------------------- 1 | const decorations = []; 2 | 3 | class GitFile { 4 | 5 | constructor(data) { 6 | if (data.importFields) { 7 | this.fields = data.importFields; 8 | } else { 9 | this.fields = [ 10 | "html_url", 11 | "name", 12 | "path", 13 | "sha", 14 | "url", 15 | "repositoryId", 16 | "ownerId", 17 | "id", 18 | ].reduce((p, c) => { 19 | p[c] = data[c]; 20 | return p; 21 | }, {}); 22 | this.fields.repositoryId = data.repository.id; 23 | this.fields.ownerId = data.repository.owner.id; 24 | this.fields.id = this.fields.url; 25 | this.fields.repoFullName = data.repository.full_name; 26 | } 27 | } 28 | decorate(body) { 29 | if (body) { 30 | decorations.forEach((f) => { 31 | this.fields[f] = body[f]; 32 | }); 33 | } 34 | } 35 | } 36 | module.exports = GitFile; 37 | -------------------------------------------------------------------------------- /src/js/compress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * makes base64 strings to avoid invalid string stuff withkv stores 3 | * @param {object} obj object to compress 4 | * @return {string} as base64 5 | */ 6 | const lz = require('lz-string') 7 | const compress = (obj) => { 8 | return compressString (JSON.stringify(obj)) 9 | } 10 | /** 11 | * 12 | * @param {string} str string to compress 13 | * @return {string} as base64 14 | */ 15 | const compressString = (str) => lz.compressToBase64(str) 16 | 17 | /** 18 | * 19 | * @param {string} str b64 string to decompress 20 | * @return {object} original object 21 | */ 22 | const decompress = (str) => { 23 | return JSON.parse(decompressString(str)); 24 | }; 25 | /** 26 | * 27 | * @param {string} str b64 string to decompress 28 | * @return {string} 29 | */ 30 | const decompressString = (str) => { 31 | return lz.decompressFromBase64(str); 32 | } 33 | 34 | module.exports = { 35 | compress, 36 | compressString, 37 | decompress, 38 | decompressString 39 | } -------------------------------------------------------------------------------- /src/js/classes/GitOwner.js: -------------------------------------------------------------------------------- 1 | class GitOwner { 2 | static decorations = [ 3 | "twitter_username", 4 | "name", 5 | "company", 6 | "location", 7 | "email", 8 | "bio", 9 | "hireable", 10 | "bio", 11 | "public_repos", 12 | "followers", 13 | "createdAt", 14 | "blog", 15 | ]; 16 | constructor({ repository, importFields }) { 17 | if (importFields) { 18 | this.fields = importFields; 19 | } else { 20 | const { owner } = repository; 21 | this.fields = [ 22 | "avatar_url", 23 | "html_url", 24 | "id", 25 | "login", 26 | "html_url", 27 | ].reduce((p, c) => { 28 | p[c] = owner[c]; 29 | return p; 30 | }, {}); 31 | if (!this.fields.name) this.fields.name === this.fields.login; 32 | } 33 | } 34 | 35 | decorate(body) { 36 | if (body) { 37 | this.constructor.decorations.forEach((f) => { 38 | this.fields[f] = body[f]; 39 | }); 40 | } 41 | } 42 | } 43 | module.exports = GitOwner; 44 | -------------------------------------------------------------------------------- /src/components/repoinfochip.vue: -------------------------------------------------------------------------------- 1 | 20 | 37 | -------------------------------------------------------------------------------- /src/components/loginchip.vue: -------------------------------------------------------------------------------- 1 | 14 | 38 | -------------------------------------------------------------------------------- /src/components/webappcard.vue: -------------------------------------------------------------------------------- 1 | 24 | 37 | 53 | -------------------------------------------------------------------------------- /src/js/settings.js: -------------------------------------------------------------------------------- 1 | const hash = require("object-hash"); 2 | 3 | const getKey = (key, keyId = "cache") => { 4 | if (typeof key === "undefined" || key === null) { 5 | throw new Error("undefined or null keys not allowed"); 6 | } 7 | return hash({ 8 | key, 9 | keyId, 10 | }); 11 | }; 12 | 13 | const queryDefinition = { 14 | query: { 15 | q: "filename:appsscript extension:.json", 16 | }, 17 | keyId: "scrgit", 18 | get dataName() { 19 | return getKey({ 20 | query: this.query, 21 | keyId: this.keyId, 22 | }); 23 | }, 24 | // the git hub api only does searches up to a 1000 results, 25 | // so we have to do multiple split by date 26 | ranges: [ 27 | "size:<=100", 28 | "size:101..250", 29 | "size:251..400", 30 | "size:401..550", 31 | "size:>550", 32 | ], 33 | gistId: "9daba5fb20a97d020431fe4a114011c7", 34 | schemaVersion: '1.3', 35 | tokenSchemaVersion: '1.1', 36 | ttl: 1000 * 60 * 60 * 2, 37 | get gistApi() { 38 | return `https://api.github.com/gists/${this.gistId}`; 39 | }, 40 | }; 41 | 42 | module.exports = { 43 | queryDefinition, 44 | getKey, 45 | }; -------------------------------------------------------------------------------- /src/js/scrapi.js: -------------------------------------------------------------------------------- 1 | /* global gapi */ 2 | // this is all about creating an apps script project 3 | export const createProject = ({ title, contents, parentId }) => { 4 | const pack = { 5 | title, 6 | }; 7 | if (parentId) pack.parentId = parentId; 8 | return gapi.client.script.projects.create(pack).then((response) => { 9 | const { result } = response; 10 | const { error, scriptId } = result; 11 | if (error) throw new Error(error); 12 | if (!contents) return result; 13 | 14 | const files = contents.map((f) => { 15 | return { 16 | name: f.gasName, 17 | type: f.gasType, 18 | source: f.content, 19 | }; 20 | }); 21 | 22 | // special patch for bugin the API where there is more than one file with the same name 23 | // typically this would be index.html and index.js confusion 24 | // we'll just rename one of them 25 | files.forEach((f,i,a) => { 26 | if(a.findIndex(g=>g.name === f.name) !== i) f.name = f.name + i 27 | }) 28 | 29 | return gapi.client.script.projects.updateContent({ 30 | scriptId, 31 | resource: { 32 | files, 33 | }, 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/js/fiddly.js: -------------------------------------------------------------------------------- 1 | // odd things can go here 2 | // this is a potential poolyfill for request animation frame 3 | import delay from "delay"; 4 | 5 | export const rqanf = (function() { 6 | return ( 7 | window.requestAnimationFrame || 8 | window.webkitRequestAnimationFrame || 9 | window.mozRequestAnimationFrame || 10 | window.oRequestAnimationFrame || 11 | window.msRequestAnimationFrame || 12 | function(callback) { 13 | return window.setTimeout(callback, 1000 / 60); 14 | } 15 | ); 16 | })(); 17 | 18 | // trying to smooth things with the d3 animation so that we get some 19 | // vue dom updates from time to time as well otherwise everything just stops for d3 to do its thing 20 | // with lots of nodes, d3 updating the dom can take forever so we'll do it in bits to show some progress 21 | // the idea is to wait for a bit, then jump in at the next animation frame opportunity 22 | export const delayAnimation = (ms, callback) => { 23 | return new Promise((resolve, reject) => { 24 | delay(ms || 0).then(() => { 25 | rqanf(() => { 26 | try { 27 | resolve(callback()); 28 | } catch (err) { 29 | reject(err); 30 | } 31 | }); 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | gitvizzy 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/js/gasvizzy.js: -------------------------------------------------------------------------------- 1 | const GitData = require("./classes/GitData"); 2 | const { enumerateManifests } = require("./gasser"); 3 | const { cacheGet } = require("./cache"); 4 | export const delay = require("delay"); 5 | const { initFiltering } = require("./filtering"); 6 | 7 | // preferably get from redis 8 | const getFromCache = async (force) => { 9 | return cacheGet(force).then((result) => { 10 | const { value, timestamp } = result || {}; 11 | if (value) { 12 | console.log( 13 | `Using cached data from ${(new Date().getTime() - timestamp) / 14 | 60 / 15 | 1000 / 16 | 60} hours ago` 17 | ); 18 | return { 19 | gd: new GitData(value), 20 | timestamp, 21 | }; 22 | } 23 | }); 24 | }; 25 | 26 | export const gasVizzyInit = (force) => { 27 | return getFromCache(force).then(({ gd, timestamp }) => { 28 | const mf = enumerateManifests(gd); 29 | const { dob, fob } = initFiltering({ gd, mf }); 30 | // for convenience we'll put a pointer to the content in the files section 31 | gd.files.forEach((f) => { 32 | // get the matching shax 33 | const shax = gd.shaxs.get(f.fields.sha); 34 | // add a pointer to the shared content 35 | f.fields.content = shax.fields.content; 36 | }); 37 | return { 38 | gd, 39 | mf, 40 | timestamp, 41 | dob, 42 | fob, 43 | }; 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/oauthscopecard.vue: -------------------------------------------------------------------------------- 1 | 19 | 48 | 64 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import vuetify from "./plugins/vuetify"; 4 | import store from "./js/store"; 5 | import JsonViewer from "vue-json-viewer"; 6 | import { forageInit } from "./js/forager"; 7 | import { initFb, fbuiInit } from "./js/auth"; 8 | import { dealWithParams } from "./js/params"; 9 | 10 | // local storage 11 | forageInit(); 12 | 13 | // firebase analytics 14 | initFb(); 15 | 16 | // initialize firebase auth 17 | fbuiInit(store.dispatch); 18 | 19 | // get any stored tokens 20 | store.dispatch('getStoredTokens'); 21 | 22 | Vue.use(JsonViewer); 23 | 24 | import TabVisibility from "@/js/classes/tabvisibility"; 25 | // eslint-disable-next-line no-unused-vars 26 | const tabVisibility = new TabVisibility(); 27 | 28 | Vue.config.productionTip = false; 29 | 30 | // initialize gapi 31 | store.dispatch("gapi"); 32 | 33 | // get any url params 34 | dealWithParams(store); 35 | 36 | // get initial git data from cache 37 | store.dispatch("vizzyInit"); 38 | 39 | new Vue({ 40 | vuetify, 41 | store, 42 | render: (h) => h(App), 43 | }).$mount("#app"); 44 | 45 | // when tab comes in view, its possible a new render is needed 46 | // this seems to be taking care of itself without the need for this 47 | // except for occassionally from developer tools interactions 48 | // so let's leave this out for now 49 | /* 50 | tabVisibility 51 | .onVisible(() => store.dispatch("updateRoot", true)) 52 | .onHidden(() => store.commit("setInfoMoused", false)); 53 | */ -------------------------------------------------------------------------------- /src/js/classes/GasManifest.js: -------------------------------------------------------------------------------- 1 | class GasManifest { 2 | constructor(shax) { 3 | this.shax = shax; 4 | this.manifest = this.shax && this.shax.fields && this.shax.fields.content; 5 | } 6 | 7 | get id() { 8 | return this.shax && this.shax.fields && this.shax.fields.id; 9 | } 10 | 11 | prop(type) { 12 | return this.manifest && this.manifest[type]; 13 | } 14 | 15 | get advancedServices() { 16 | return this.dependencies && this.dependencies.enabledAdvancedServices; 17 | } 18 | 19 | get libraries() { 20 | /* 21 | "developmentMode": boolean, 22 | "libraryId": string, 23 | "userSymbol": string, 24 | "version": string 25 | */ 26 | return this.dependencies && this.dependencies.libraries; 27 | } 28 | get dependencies() { 29 | /* 30 | "serviceId": string, 31 | "userSymbol": string, 32 | "version": string 33 | */ 34 | return this.prop("dependencies"); 35 | } 36 | get timeZone() { 37 | return this.prop("timeZone"); 38 | } 39 | get addOns() { 40 | return this.prop("addOns"); 41 | } 42 | 43 | get runtimeVersion() { 44 | return this.prop("runtimeVersion"); 45 | } 46 | get webapp() { 47 | return this.prop("webapp"); 48 | } 49 | get oauthScopes() { 50 | return this.prop("oauthScopes"); 51 | } 52 | get dataStudio() { 53 | return this.prop("dataStudio"); 54 | } 55 | get firstRepoName() { 56 | return ( 57 | this.shax && 58 | this.shax && 59 | this.shax.fields && 60 | this.shax.fields.repoFullName 61 | ); 62 | } 63 | } 64 | 65 | module.exports = GasManifest; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitvizzy", 3 | "version": "1.0.8", 4 | "private": false, 5 | "repository": { 6 | "url": "https://github.com/brucemcpherson/gitvizzy" 7 | }, 8 | "scripts": { 9 | "serve": "vue-cli-service serve", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "@octokit/rest": "^18.2.0", 15 | "anchorme": "^2.1.2", 16 | "canvg": "^3.0.7", 17 | "core-js": "^3.6.5", 18 | "crossfilter2": "^1.5.4", 19 | "d3": "^6.3.1", 20 | "delay": "^4.4.0", 21 | "firebase": "^8.2.4", 22 | "ky": "^0.26.0", 23 | "localforage": "^1.9.0", 24 | "lz-string": "^1.4.4", 25 | "object-hash": "^2.1.1", 26 | "vue": "^2.6.11", 27 | "vue-json-viewer": "^2.2.18", 28 | "vuetify": "^2.2.11", 29 | "vuex": "^3.6.0" 30 | }, 31 | "devDependencies": { 32 | "@vue/cli-plugin-babel": "~4.5.0", 33 | "@vue/cli-plugin-eslint": "~4.5.0", 34 | "@vue/cli-service": "~4.5.0", 35 | "babel-eslint": "^10.1.0", 36 | "eslint": "^6.7.2", 37 | "eslint-plugin-vue": "^6.2.2", 38 | "sass": "^1.19.0", 39 | "sass-loader": "^8.0.0", 40 | "vue-cli-plugin-vuetify": "~2.0.9", 41 | "vue-template-compiler": "^2.6.11", 42 | "vuetify-loader": "^1.3.0" 43 | }, 44 | "eslintConfig": { 45 | "root": true, 46 | "env": { 47 | "node": true 48 | }, 49 | "extends": [ 50 | "plugin:vue/essential", 51 | "eslint:recommended" 52 | ], 53 | "parserOptions": { 54 | "parser": "babel-eslint" 55 | }, 56 | "rules": {} 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not dead" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/components/advancedservicecard.vue: -------------------------------------------------------------------------------- 1 | 41 | 54 | 70 | -------------------------------------------------------------------------------- /src/js/classes/tabvisibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * handles tab visibility changes 3 | */ 4 | 5 | export default class TabVisibility { 6 | constructor() { 7 | this._variants = [ 8 | { 9 | prop: "hidden", 10 | eventName: "visibilitychange", 11 | }, 12 | { 13 | prop: "msHidden", 14 | eventName: "msvisibilitychange", 15 | }, 16 | { 17 | prop: "webkitHidden", 18 | eventName: "webkitvisibilitychange", 19 | }, 20 | ]; 21 | this._onVisible = null; 22 | this._onHidden = null; 23 | if (!this.isSupported) { 24 | throw new Error("cant handle visibility change events"); 25 | } 26 | document.addEventListener( 27 | this.variant.eventName, 28 | (e) => this._onChange(e), 29 | false 30 | ); 31 | } 32 | 33 | _isdef(variant) { 34 | return variant ? typeof document[variant.prop] !== typeof undefined : false; 35 | } 36 | 37 | get variant() { 38 | return this._variants.find((f) => this._isdef(f)); 39 | } 40 | 41 | get isSupported() { 42 | return Boolean(this.variant); 43 | } 44 | 45 | isVisible() { 46 | return this.isSupported ? !document[this.variant.prop] : true; 47 | } 48 | 49 | _onChange(e) { 50 | if (this.isVisible() && this._onVisible) { 51 | this._onVisible(e); 52 | } else if (this._onHidden) { 53 | this._onHidden(e); 54 | } 55 | } 56 | 57 | onVisibility(state, func) { 58 | if (state) { 59 | this._onVisible = func; 60 | } else { 61 | this._onHidden = func; 62 | } 63 | return this; 64 | } 65 | 66 | onVisible(func) { 67 | return this.onVisibility(true, func); 68 | } 69 | 70 | onHidden(func) { 71 | return this.onVisibility(false, func); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/components/errorplace.vue: -------------------------------------------------------------------------------- 1 | 21 | 66 | -------------------------------------------------------------------------------- /src/js/forager.js: -------------------------------------------------------------------------------- 1 | // this is all about using local storage 2 | // a copy of the latest data from gist cache is held locally and refreshed from time to time 3 | import localForage from "localforage"; 4 | import { queryDefinition } from "./settings"; 5 | const tokenKey = "tokes"; 6 | const cacheKey = "cache"; 7 | const { ttl, schemaVersion, tokenSchemaVersion } = queryDefinition; 8 | 9 | let forager = null; 10 | let tokenForager = null; 11 | export const forageInit = () => { 12 | const name = "vizzy"; 13 | forager = localForage.createInstance({ 14 | name, 15 | storeName: "scrviz", 16 | description: "for data github scrviz data caching", 17 | }); 18 | tokenForager = localForage.createInstance({ 19 | name, 20 | storeName: "scrviztokens", 21 | description: "for scrviz tokens", 22 | }); 23 | }; 24 | 25 | const getStuff = (frg, key, schemaVersion, force) => 26 | force ? Promise.resolve(null) : frg.getItem(key).then((r) => { 27 | return ( 28 | r && 29 | r.value && 30 | r.schemaVersion === schemaVersion && 31 | (!r.expiry || r.expiry > new Date().getTime()) && 32 | r.value 33 | ); 34 | }); 35 | 36 | // get from cache if it hasnt expired and if its a good version 37 | export const getCacheData = (force) => getStuff(forager, cacheKey, schemaVersion, force); 38 | 39 | // get tokens from cache 40 | export const getTokenData = () => 41 | getStuff(tokenForager, tokenKey, tokenSchemaVersion); 42 | 43 | // put data to cache 44 | const setVizzyStuff = (frg, key, value, schemaVersion, ttl) => 45 | frg.setItem(key, { 46 | value, 47 | expiry: ttl ? new Date().getTime() + ttl : null, 48 | schemaVersion, 49 | }); 50 | 51 | export const setTokenData = (value) => 52 | setVizzyStuff(tokenForager, tokenKey, value, tokenSchemaVersion); 53 | 54 | export const setCacheData = (value) => 55 | setVizzyStuff(forager, cacheKey, value, schemaVersion, ttl); 56 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/webappfilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 82 | -------------------------------------------------------------------------------- /src/components/librarycard.vue: -------------------------------------------------------------------------------- 1 | 42 | 71 | 87 | -------------------------------------------------------------------------------- /src/components/libraryfilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 82 | -------------------------------------------------------------------------------- /src/components/timezonefilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 82 | -------------------------------------------------------------------------------- /src/components/datastudiofilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 82 | -------------------------------------------------------------------------------- /src/components/repofilter.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 84 | -------------------------------------------------------------------------------- /src/components/runtimeversionfilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 84 | -------------------------------------------------------------------------------- /src/components/advancedservicefilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 84 | -------------------------------------------------------------------------------- /src/components/addonfilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 88 | -------------------------------------------------------------------------------- /src/components/datastudiocard.vue: -------------------------------------------------------------------------------- 1 | 56 | 69 | 85 | -------------------------------------------------------------------------------- /src/js/classes/GitData.js: -------------------------------------------------------------------------------- 1 | const GitFile = require("./GitFile.js"); 2 | const GitOwner = require("./GitOwner.js"); 3 | const GitRepo = require("./GitRepo.js"); 4 | const GitShax = require("./GitShax.js"); 5 | const types = ["shaxs", "files", "owners", "repos"]; 6 | 7 | class GitData { 8 | constructor(data) { 9 | this.types = types; 10 | if (data) { 11 | this.import(data); 12 | } else { 13 | this.repos = new Map(); 14 | this.owners = new Map(); 15 | this.files = new Map(); 16 | this.shaxs = new Map(); 17 | } 18 | } 19 | 20 | export() { 21 | return this.types.reduce((p, c) => { 22 | p[c] = this.fields(c); 23 | return p; 24 | }, {}); 25 | } 26 | 27 | import(data) { 28 | this.owners = new Map( 29 | data.owners.map((f) => [f.id, new GitOwner({ importFields: f })]) 30 | ); 31 | this.repos = new Map( 32 | data.repos.map((f) => [f.id, new GitRepo({ importFields: f })]) 33 | ); 34 | this.shaxs = new Map( 35 | data.shaxs.map((f) => [f.id, new GitShax({ importFields: f })]) 36 | ); 37 | this.files = new Map( 38 | data.files.map((f) => [f.id, new GitFile({ importFields: f })]) 39 | ); 40 | return this; 41 | } 42 | 43 | 44 | add(data) { 45 | const file = new GitFile(data); 46 | this.files.set(file.fields.id, file); 47 | this.getOrAddRepo(data); 48 | this.getOrAddOwner(data); 49 | this.getOrAddShax(data); 50 | } 51 | 52 | getOrAddRepo(data) { 53 | if (!this.repos.has(data.repository.id)) { 54 | const repo = new GitRepo(data); 55 | this.repos.set(repo.fields.id, repo); 56 | } 57 | return this.repos.get(data.repository.id); 58 | } 59 | 60 | getOrAddOwner(data) { 61 | if (!this.owners.has(data.repository.owner.id)) { 62 | const owner = new GitOwner(data); 63 | this.owners.set(owner.fields.id, owner); 64 | } 65 | return this.owners.get(data.repository.owner.id); 66 | } 67 | 68 | getOrAddShax(data) { 69 | if (!this.shaxs.has(data.sha)) { 70 | const shax = new GitShax(data); 71 | this.shaxs.set(shax.fields.id, shax); 72 | } 73 | return this.shaxs.get(data.sha); 74 | } 75 | 76 | items(type) { 77 | const input = this[type]; 78 | if (!input) throw new Error(`invalid type ${type}`); 79 | return Array.from(input.values()); 80 | } 81 | 82 | fields(type) { 83 | const items = this.items(type); 84 | return items.map((f) => f.fields); 85 | } 86 | 87 | } 88 | 89 | module.exports = GitData; 90 | -------------------------------------------------------------------------------- /src/components/oauthscopefilter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 91 | -------------------------------------------------------------------------------- /src/js/params.js: -------------------------------------------------------------------------------- 1 | // we can have parameters to directly go somewhere 2 | const validParams = new Set(["manifest", "owner", "repo"]); 3 | const validateParams = () => { 4 | const params = new URLSearchParams(window.location.search); 5 | // check we know them an they have values 6 | const keys = Array.from(params.keys()); 7 | const isValid = 8 | keys.length < 2 && keys.every((f) => validParams.has(f) && params.get(f)); 9 | const type = keys.length && Array.from(params.keys())[0]; 10 | 11 | return { 12 | params, 13 | isValid, 14 | type, 15 | value: type && params.get(type), 16 | doit: isValid && keys.length, 17 | keys, 18 | }; 19 | }; 20 | 21 | export const dealWithParams = (store) => { 22 | const vp = validateParams(); 23 | 24 | if (!vp.isValid) { 25 | store.commit("setShowError", { 26 | title: "Invalid parameters", 27 | message: vp.keys && vp.keys.join(","), 28 | }); 29 | } else { 30 | store.dispatch("fixParamsLevel", vp); 31 | store.commit("setUrlParams", vp); 32 | } 33 | }; 34 | 35 | export const directLink = ({ item, type }) => { 36 | const l = window.location; 37 | let t = `${l.protocol}//${l.hostname}`; 38 | if (l.port) t += `:${l.port}`; 39 | 40 | return `${t}?${type}=${encodeURIComponent(itemAccessor({ item, type }))}`; 41 | }; 42 | 43 | const itemAccessor = ({ item, type }) => { 44 | // this is where to find the data in a d3 each 45 | const b = item && item.data; 46 | 47 | // we're using manifest as alias for file in direct link 48 | if (type === "manifest" && b.type === "files") { 49 | const t = b && b.file && b.file.fields; 50 | const l = t && `${t.repoFullName}/${t.path}`; 51 | return l; 52 | } else if (type === "owner" && b.type === "owners") { 53 | const t = b && b.owner && b.owner.fields; 54 | const l = t && `${t.login}`; 55 | return l; 56 | } else if (type === "repo" && b.type === "repos") { 57 | const t = b && b.repo && b.repo.fields; 58 | const l = t && `${t.full_name}`; 59 | return l; 60 | } 61 | }; 62 | 63 | const itemMatch = ({ item, type, target }) => { 64 | const value = itemAccessor({ item, type }); 65 | return target === value; 66 | }; 67 | 68 | export const itemFind = ({ node, target, type }) => { 69 | let foundling = null; 70 | if (node) { 71 | node.each(function(item) { 72 | if (!foundling && itemMatch({ item, type, target })) { 73 | foundling = { 74 | item, 75 | d3This: this, 76 | }; 77 | } 78 | }); 79 | } 80 | return foundling; 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/ownerfilter.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 92 | -------------------------------------------------------------------------------- /src/js/cache.js: -------------------------------------------------------------------------------- 1 | // this used to be on redis, but we need it client side now 2 | // so the is a just a gist compressed 3 | const { decompress } = require("./compress"); 4 | const { queryDefinition } = require("./settings"); 5 | const { Octokit } = require("@octokit/rest"); 6 | const delay = require("delay"); 7 | const { getCacheData, setCacheData } = require("./forager"); 8 | 9 | import ky from "ky"; 10 | export const getky = (url) => ky.get(url).json(); 11 | 12 | let octokit = null; 13 | 14 | export const cacheInit = (store) => { 15 | octokit = new Octokit({ 16 | auth: `token ${store.state.githubToken}`, 17 | userAgent: "scrviz v1.0.1", 18 | }); 19 | }; 20 | 21 | const getRateInfo = (response) => { 22 | const { headers } = response; 23 | 24 | const ratelimitRemaining = headers["x-ratelimit-remaining"]; 25 | const ratelimitReset = headers["x-ratelimit-reset"]; 26 | return { 27 | ratelimitRemaining, 28 | ratelimitReset, 29 | waitTime: 30 | ratelimitRemaining > 1 31 | ? 0 32 | : Math.max(2500, ratelimitReset * 1000 - new Date().getTime()), 33 | }; 34 | }; 35 | const getWithWait = (what, tries = 0) => { 36 | return what().catch((qe) => { 37 | const { error } = qe; 38 | const { status } = error; 39 | // we get a 403 for rate limit exceeded (why not 429?) 40 | if ((status !== 403 && status !== 429) || tries > 3) 41 | return Promise.reject(qe); 42 | const { waitTime } = getRateInfo(error); 43 | 44 | // try again 45 | return delay(waitTime).then(() => getWithWait(what, tries + 1)); 46 | }); 47 | }; 48 | 49 | export const decorator = (url) => { 50 | const u = url.replace("https://api.github.com", "GET "); 51 | return getWithWait(() => 52 | octokit.request(u).then((r) => { 53 | return r.data; 54 | }) 55 | ); 56 | }; 57 | 58 | // this should get us the latest raw url for this gist 59 | const raw = () => { 60 | return getky(queryDefinition.gistApi).then((r) => { 61 | return r.files && r.files[Object.keys(r.files)[0]].raw_url; 62 | }); 63 | }; 64 | 65 | // cache is using gist now 66 | export const cacheGet = async (force=false) => { 67 | // maybe its in local storage 68 | let text = await getCacheData(force); 69 | if (!text) { 70 | const rawUrl = await raw(); 71 | const response = await ky.get(rawUrl); 72 | text = await response.text(); 73 | // write that sucker to local storage for next time 74 | setCacheData(text).then(() => { 75 | console.log("...wrote cache to local storage"); 76 | }); 77 | } else { 78 | console.log("...found github data locally"); 79 | } 80 | return text && decompress(text); 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/picker.vue: -------------------------------------------------------------------------------- 1 | 15 | 89 | -------------------------------------------------------------------------------- /src/js/gasser.js: -------------------------------------------------------------------------------- 1 | const GasManifests = require("./classes/GasManifests"); 2 | const enumerateManifests = (gd) => { 3 | const mf = new GasManifests(gd); 4 | const maps = { 5 | advancedServices: new Map(), 6 | libraries: new Map(), 7 | timeZones: new Map(), 8 | webapps: new Map(), 9 | runtimeVersions: new Map(), 10 | addOns: new Map(), 11 | oauthScopes: new Map(), 12 | dataStudios: new Map() 13 | }; 14 | mf.maps = maps; 15 | 16 | const add = ({ prop, item, map, id, version }) => { 17 | if (item[prop]) { 18 | const props = Array.isArray(item[prop]) ? item[prop] : [item[prop]]; 19 | props.forEach((g) => { 20 | const idValue = id ? g[id] : g; 21 | if (!map.has(idValue)) { 22 | map.set(idValue, { 23 | id: idValue, 24 | versions: new Map(), 25 | label: g.userSymbol || idValue, 26 | }); 27 | } 28 | const a = map.get(idValue); 29 | const versionValue = version ? g[version] : g; 30 | if (!a.versions.has(versionValue)) { 31 | a.versions.set(versionValue, g); 32 | } 33 | }); 34 | } 35 | }; 36 | 37 | const addAddon = ({ item, map }) => { 38 | 39 | if (item.addOns) { 40 | Object.keys(item.addOns).forEach((f) => { 41 | if (!map.has(f)) { 42 | map.set(f, { 43 | id: f, 44 | versions: new Map(), 45 | label: f, 46 | }); 47 | map.get(f).versions.set(f, item.addOns[f]); 48 | } 49 | }); 50 | } 51 | }; 52 | 53 | mf.manifests.forEach((f) => { 54 | if (f.manifest) { 55 | add({ 56 | prop: "advancedServices", 57 | item: f, 58 | map: maps.advancedServices, 59 | version: "version", 60 | id: "serviceId", 61 | }); 62 | add({ 63 | prop: "libraries", 64 | item: f, 65 | map: maps.libraries, 66 | version: "version", 67 | id: "libraryId", 68 | }); 69 | add({ 70 | prop: "timeZone", 71 | item: f, 72 | map: maps.timeZones, 73 | version: "timeZone", 74 | id: null, 75 | }); 76 | add({ 77 | prop: "dataStudio", 78 | item: f, 79 | map: maps.dataStudios, 80 | version: "description", 81 | id: "name", 82 | }); 83 | add({ 84 | prop: "runtimeVersion", 85 | item: f, 86 | map: maps.runtimeVersions, 87 | version: "runtimeVersion", 88 | id: null, 89 | }); 90 | add({ 91 | prop: "webapp", 92 | item: f, 93 | map: maps.webapps, 94 | version: "executeAs", 95 | id: "access", 96 | }); 97 | add({ 98 | prop: "oauthScopes", 99 | item: f, 100 | map: maps.oauthScopes, 101 | version: null, 102 | id: null, 103 | }); 104 | addAddon({ 105 | item: f, 106 | map: maps.addOns, 107 | }); 108 | } 109 | }); 110 | return mf; 111 | }; 112 | 113 | module.exports = { 114 | enumerateManifests, 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/tagitem.vue: -------------------------------------------------------------------------------- 1 | 52 | 91 | 107 | -------------------------------------------------------------------------------- /src/js/auth.js: -------------------------------------------------------------------------------- 1 | /* global gapi */ 2 | import firebase from "firebase/app"; 3 | import "firebase/auth"; 4 | import "firebase/analytics"; 5 | import { 6 | firebaseConfig, 7 | googleScopes, 8 | githubScopes, 9 | googleConfig, 10 | } from "../../secrets/config"; 11 | let fbal = null; 12 | 13 | // firebase auth inly used for github 14 | // using gapi for google auth so token gets refreshed automatically 15 | export const initFb = () => { 16 | firebase.initializeApp(firebaseConfig); 17 | fbal = firebase.analytics(); 18 | }; 19 | 20 | // log fb analytica event 21 | export const logEvent = (name, pack) => { 22 | return fbal.logEvent(name, pack); 23 | }; 24 | 25 | // keep a note of providers here 26 | let providers = { 27 | github: null, 28 | }; 29 | 30 | /** 31 | * call this once on app initialization 32 | */ 33 | export const fbuiInit = () => { 34 | providers.github = new firebase.auth.GithubAuthProvider(); 35 | githubScopes.forEach((s) => providers.github.addScope(s)); 36 | }; 37 | 38 | const signer = (provider, onSigned) => { 39 | // actually this is general purpose, but we're oly using it for github 40 | return firebase 41 | .auth() 42 | .signInWithPopup(provider) 43 | .then((result) => { 44 | return onSigned(result); 45 | }) 46 | 47 | .catch((error) => { 48 | if (error.code === "auth/account-exists-with-different-credential") { 49 | // that's fine because all we want is the credential 50 | 51 | return onSigned(error); 52 | } 53 | console.log(error); 54 | Promise.reject(error); 55 | }); 56 | }; 57 | 58 | // we need to sign into github to get an oauth token to avoid quota problems which are legion 59 | export const signinGithub = (commit, dispatch) => { 60 | return signer(providers.github, (result) => { 61 | // github tokens dont expire, so this will presist until specific logout 62 | commit( 63 | "setGithubToken", 64 | result && result.credential && result.credential.accessToken 65 | ); 66 | // store this for future sessions 67 | dispatch("setStoredTokens"); 68 | }); 69 | }; 70 | 71 | // sign in to google using gapi 72 | export const signin = () => { 73 | return gapi.auth2.getAuthInstance().signIn(); 74 | }; 75 | 76 | // signout of both google and github 77 | export const signout = (commit) => { 78 | // signout of google 79 | const ai = gapi.auth2.getAuthInstance(); 80 | ai.signOut(); 81 | // signout of firebase (github) 82 | firebase.auth().signOut(); 83 | // thses will need refreshed 84 | commit("clearTokens", null); 85 | }; 86 | 87 | // these will be needed when creating a picker instance for container bound scripts 88 | export const getPickerKey = () => googleConfig.apiKey; 89 | export const getProjectId = () => googleConfig.projectId; 90 | 91 | // called on startup to get gapi going 92 | export const gapiInit = (onUser) => { 93 | return gapi.client.init(googleConfig).then(function() { 94 | // Listen for sign-in state changes. 95 | const instance = gapi.auth2.getAuthInstance(); 96 | instance.currentUser.listen(onUser); 97 | 98 | // Handle the initial sign-in state. 99 | onUser(instance.currentUser.get()); 100 | }); 101 | }; 102 | 103 | export const gapiCheckScopes = (user) => { 104 | if (!user) 105 | return { 106 | ok: false, 107 | granted: null, 108 | }; 109 | // these are the scopes we've been granted 110 | const granted = (user.getGrantedScopes() || "").split(" "); 111 | const ok = googleScopes.every((f) => granted.indexOf(f) !== -1); 112 | const denied = googleScopes.filter((f) => granted.indexOf(f) === -1); 113 | return { 114 | ok, 115 | granted, 116 | requested: googleScopes, 117 | denied, 118 | }; 119 | }; 120 | 121 | export const gapiAdditionalScopes = (user) => { 122 | if (!user) return Promise.resolve(null); 123 | 124 | // get currently assigned scopes 125 | const checkScopes = gapiCheckScopes(user) 126 | 127 | // if we have all we need it's done 128 | if (checkScopes.ok) return Promise.resolve(checkScopes) 129 | 130 | // now we have to get the previously denied scopes 131 | const option = new gapi.auth2.SigninOptionsBuilder(); 132 | option.setScope(checkScopes.denied.join(" ")); 133 | return user.grant(option) 134 | .then(() => { 135 | return gapiCheckScopes(user) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /src/components/repocard.vue: -------------------------------------------------------------------------------- 1 | 73 | 131 | 147 | -------------------------------------------------------------------------------- /src/components/addoncard.vue: -------------------------------------------------------------------------------- 1 | 76 | 142 | 158 | -------------------------------------------------------------------------------- /src/components/repotree copy.vue: -------------------------------------------------------------------------------- 1 | 24 | 201 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 230 | -------------------------------------------------------------------------------- /src/js/filtering.js: -------------------------------------------------------------------------------- 1 | // using cross filter to manage the interlocked selectable filters 2 | // this is all a bit more verbose than I'd hoped when I started out. 3 | 4 | import cf from "crossfilter2"; 5 | import { enumerateManifests } from "./gasser"; 6 | 7 | const reduceRepos = ({ fob, dob, filters, filterPlus }) => { 8 | if (filters.owners.size && filterPlus) { 9 | const o = new Set( 10 | fob.owners 11 | .allFiltered() 12 | .map(dob.owners.accessor) 13 | .filter((f) => filters.owners.has(f)) 14 | ); 15 | 16 | dob.reposByOwner.filter((d) => o.has(d)); 17 | } 18 | }; 19 | 20 | export const remakeMf = (state) => { 21 | const { fob, dob, filterPlus } = state; 22 | const dFilters = getDFilters(state); 23 | 24 | const repos = new Set(fob.repos.allFiltered().map(dob.repos.accessor)); 25 | dob.filesByRepo.filter((d) => repos.has(d)); 26 | 27 | const fileShas = new Set( 28 | fob.files.allFiltered().map(dob.filesBySha.accessor) 29 | ); 30 | dob.shaxsByFile.filter((d) => fileShas.has(d)); 31 | 32 | const mm = (name) => 33 | new Map(fob[name].allFiltered().map((d) => [d.fields.id, d])); 34 | 35 | // new we can re-enumerate based on all those filters 36 | const mf = enumerateManifests({ 37 | files: mm("files"), 38 | repos: mm("repos"), 39 | owners: mm("owners"), 40 | shaxs: mm("shaxs"), 41 | }); 42 | 43 | // need to make a cross filter for all that 44 | const cfManifests = cf(Array.from(mf.manifests.values())); 45 | 46 | const dm = { 47 | timeZones: cfManifests.dimension((d) => d.timeZone || null), 48 | dataStudios: cfManifests.dimension( 49 | (d) => (d.dataStudio && d.dataStudio.name) || null 50 | ), 51 | runtimeVersions: cfManifests.dimension((d) => d.runtimeVersion || null), 52 | oauthScopes: cfManifests.dimension((d) => d.oauthScopes || [], true), 53 | addOns: cfManifests.dimension( 54 | (d) => (d.addOns && Object.keys(d.addOns)) || [], 55 | true 56 | ), 57 | webapps: cfManifests.dimension( 58 | (d) => (d.webapp && d.webapp.access) || null 59 | ), 60 | advancedServices: cfManifests.dimension( 61 | (d) => 62 | (d.advancedServices && d.advancedServices.map((f) => f.serviceId)) || 63 | [], 64 | true 65 | ), 66 | libraries: cfManifests.dimension( 67 | (d) => (d.libraries && d.libraries.map((f) => f.libraryId)) || [], 68 | true 69 | ), 70 | }; 71 | 72 | // there might be some manifest filtering that feeds back into repos and users 73 | // for example - timezones 74 | if (filterPlus) { 75 | Object.keys(dm).forEach((k) => { 76 | if (dFilters[k].size) { 77 | dm[k].filter((d) => { 78 | return dFilters[k].has(d); 79 | }); 80 | } 81 | }); 82 | } 83 | 84 | return { 85 | mf, 86 | dm, 87 | cfManifests, 88 | }; 89 | }; 90 | 91 | const reduceOwners = ({ fob, dob, filters, filterPlus }) => { 92 | // if filtering on repos, reduce the owners to just the selected repos 93 | if (filters.repos.size && filterPlus) { 94 | // apply the repo filter, then pull out the owner ids they refer to 95 | // so this will end up being a set of ownerIds belonging to the repos filter 96 | const r = new Set( 97 | fob.repos 98 | .allFiltered() 99 | .filter((d) => filters.repos.has(dob.repos.accessor(d))) 100 | .map(dob.reposByOwner.accessor) 101 | ); 102 | // now filter the owners by that list 103 | dob.owners.filter((d) => r.has(d)); 104 | } 105 | }; 106 | 107 | export const reduceManifests = (state) => { 108 | const { cfManifests, dob, fob, mf } = state; 109 | const dFilters = getDFilters(state); 110 | 111 | // if that happened then we need to re limit files, repos et 112 | if (Object.keys(dFilters).some((k) => dFilters[k].size)) { 113 | // this is the list of shas that have something of interest 114 | const mans = new Set(cfManifests.allFiltered().map((f) => f.id)); 115 | 116 | // filter out the files that share that sha 117 | dob.filesBySha.filter((d) => mans.has(d)); 118 | 119 | // then we can filter out the repos that have them 120 | const repos = new Set( 121 | fob.files.allFiltered().map(dob.filesByRepo.accessor) 122 | ); 123 | dob.repos.filter((d) => repos.has(d)); 124 | 125 | // and also filter out all the owners 126 | const owners = new Set( 127 | fob.repos.allFiltered().map(dob.reposByOwner.accessor) 128 | ); 129 | dob.owners.filter((d) => owners.has(d)); 130 | const { mf: filteredMf } = remakeMf(state); 131 | return filteredMf; 132 | } else { 133 | return mf; 134 | } 135 | }; 136 | export const getDFilters = (state) => { 137 | const { 138 | dataStudioFilter, 139 | addOnFilter, 140 | oauthScopeFilter, 141 | webappFilter, 142 | libraryFilter, 143 | advancedServiceFilter, 144 | runtimeVersionFilter, 145 | timeZoneFilter, 146 | } = state; 147 | 148 | const dFilters = { 149 | timeZones: new Set(timeZoneFilter || []), 150 | runtimeVersions: new Set(runtimeVersionFilter || []), 151 | advancedServices: new Set(advancedServiceFilter || []), 152 | libraries: new Set(libraryFilter || []), 153 | webapps: new Set(webappFilter || []), 154 | oauthScopes: new Set(oauthScopeFilter || []), 155 | addOns: new Set(addOnFilter || []), 156 | dataStudios: new Set(dataStudioFilter || []), 157 | }; 158 | return dFilters; 159 | }; 160 | 161 | export const applyFilters = (state) => { 162 | const { 163 | hireableOwners, 164 | interlockedFilters, 165 | filterPlus, 166 | fob, 167 | dob, 168 | ownerFilter, 169 | repoFilter, 170 | } = state; 171 | 172 | // first clear existing filter on repo and owner 173 | if(dob)Object.keys(dob).forEach((f) => dob[f].filterAll()); 174 | 175 | // these are the vanilla filters 176 | const filters = { 177 | owners: new Set(ownerFilter || []), 178 | repos: new Set(repoFilter || []), 179 | }; 180 | 181 | 182 | // apply hireable filter to both owner & repo 183 | if (hireableOwners && filterPlus) { 184 | dob.hireable.filter((d) => d); 185 | // get the filtered owners and apply to the repos 186 | const o = new Set(fob.owners.allFiltered().map(dob.owners.accessor)); 187 | dob.reposByOwner.filter((d) => o.has(d)); 188 | } 189 | 190 | // reduce the repos to just belong to selected owners 191 | reduceRepos({ fob, dob, filters, filterPlus }); 192 | 193 | // if filtering on repos, reduce the owners to just the selected repos 194 | reduceOwners({ fob, dob, filters, filterPlus }); 195 | 196 | // we need to remake the mf - so that needs a reduction of the manifest by these params. 197 | // first the relevant shaxs need filtered by repo - to do that we need to first filter the files 198 | const { mf, cfManifests } = remakeMf(state); 199 | 200 | // if that happened then we need to re limit files, repos et 201 | if (interlockedFilters) { 202 | return { 203 | mf: reduceManifests({ ...state, mf, cfManifests }), 204 | cfManifests, 205 | }; 206 | } else { 207 | return { 208 | mf, 209 | cfManifests, 210 | }; 211 | } 212 | }; 213 | 214 | export const initFiltering = ({ gd }) => { 215 | const keys = Object.keys(gd).filter((f) => f !== "types"); 216 | // standard cf 217 | const fob = keys.reduce((p, c) => { 218 | p[c] = cf(gd.items(c)); 219 | return p; 220 | }, {}); 221 | 222 | // id dimensions 223 | const dob = keys.reduce((p, c) => { 224 | p[c] = fob[c].dimension((d) => d.fields.id); 225 | return p; 226 | }, {}); 227 | 228 | // various other more complex 229 | dob.reposByOwner = fob.repos.dimension((d) => d.fields.ownerId); 230 | dob.filesByRepo = fob.files.dimension((d) => d.fields.repositoryId); 231 | dob.shaxsByFile = fob.shaxs.dimension((d) => d.fields.id); 232 | dob.hireable = fob.owners.dimension((d) => d.fields.hireable); 233 | dob.filesBySha = fob.files.dimension((d) => d.fields.sha); 234 | 235 | return { 236 | dob, 237 | fob, 238 | }; 239 | }; 240 | -------------------------------------------------------------------------------- /src/components/ownercard.vue: -------------------------------------------------------------------------------- 1 | 166 | 267 | 283 | -------------------------------------------------------------------------------- /src/components/repotree.vue: -------------------------------------------------------------------------------- 1 | 25 | 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | There are so many Apps Script projects out there where the source code is published on Github, but it’s hard to find what you want. Whether it’s a library, an example of an add-on, how to use an advanced service, or just see who is working on what. I fugured it would be nice if we had a searchable visualization of everything that’s public. 5 | 6 | ## writeup 7 | 8 | https://ramblings.mcpher.com/vizzy-scrviz/ 9 | 10 | ## live app 11 | 12 | https://scrviz.web.app 13 | 14 | ## the data 15 | 16 | https://ramblings.mcpher.com/vizzy-scrviz/searching-github-gas/ 17 | 18 | ## screen shots 19 | 20 | Summary view 21 | ![](./shots/2021-01-26-11-26-29.png) 22 | 23 | Detail view with filter and owner info 24 | ![](./shots/2021-01-26-11-28-26.png) 25 | 26 | Filtered view of manifest entry 27 | ![](./shots/2021-01-26-11-29-54.png) 28 | 29 | Filtered view of manifest contents 30 | ![](./shots/2021-01-26-11-30-40.png) 31 | 32 | ## scrviz now supports creating apps script projects directly 33 | 34 | https://ramblings.mcpher.com/vizzy-scrviz/vizzy-clone-apps-script-github/ 35 | 36 | ## scrviz now support tags and rows to enrich your profile and your repos info. 37 | 38 | For more info see 39 | https://github.com/brucemcpherson/scrviz-profile 40 | 41 | ## icons available for scrvizprofile 42 | 43 | Any icon from material design icons site can be added to your scrviz-profile.json. These must be specified in full (including the 'mdi-') 44 | 45 | ``` 46 | rows: [ 47 | { 48 | ...etc, 49 | "icon": "mdi-google-ads" 50 | } 51 | ] 52 | ``` 53 | 54 | However, to preserve some kind of conformity across users, a set of short names is available and preferred. Here's the list in no particular order. Many of these are also used in the app itself. If you'd like one added, let me know. Note that some are actually images rather than logos. 55 | 56 | You'd use these like this 57 | 58 | ``` 59 | rows: [ 60 | { 61 | ...etc, 62 | "icon": "youtube" 63 | } 64 | ] 65 | ``` 66 | 67 | | icon | built in name | 68 | | ------------------------------------------------------------- | ------------ | 69 | | | json | 70 | | | tags | 71 | | | excel | 72 | | | word | 73 | | | office | 74 | | | windows | 75 | | | youtube | 76 | | | linkedin | 77 | | | info | 78 | | | filter-off | 79 | | | filter-on | 80 | | | open | 81 | | | company | 82 | | | word | 83 | | | office | 84 | | | windows | 85 | | | youtube | 86 | | | linkedin | 87 | | | info | 88 | | | location | 89 | | | email | 90 | | | files | 91 | | | file | 92 | | | github | 93 | | | clasp | 94 | | | stats | 95 | | | webapp | 96 | | | access | 97 | | | viz-info | 98 | | | repos | 99 | | | libraries | 100 | | | twitter | 101 | | | maps | 102 | | | bio | 103 | | | fees | 104 | | | support | 105 | | | text | 106 | | | fees | 107 | | | hireable | 108 | | | hireable-off | 109 | | | text | 110 | | | followers | 111 | | | version | 112 | | | symbol | 113 | | | blog | 114 | | | id | 115 | | | auth | 116 | | | html | 117 | | | scrviz | 118 | | | phone | 119 | | | appsscript | 120 | | | drive | 121 | | | sheets | 122 | | | docs | 123 | | | calendar | 124 | | | gmail | 125 | | | slides | 126 | | | gcp | 127 | -------------------------------------------------------------------------------- /src/components/pulldialog.vue: -------------------------------------------------------------------------------- 1 | 173 | 344 | -------------------------------------------------------------------------------- /src/components/filecard.vue: -------------------------------------------------------------------------------- 1 | 213 | 351 | 352 | 368 | -------------------------------------------------------------------------------- /src/components/icons.vue: -------------------------------------------------------------------------------- 1 | 398 | 431 | -------------------------------------------------------------------------------- /src/js/d3prep.js: -------------------------------------------------------------------------------- 1 | const d3 = require("d3"); 2 | import { reduceManifests } from "./filtering"; 3 | export const depths = { 4 | OWNER: 0, 5 | REPO: 1, 6 | FILE: 2, 7 | DETAIL: 3 8 | } 9 | const sorter = (items) => 10 | items.sort((a, b) => { 11 | const aName = a.fields.name; 12 | const bName = b.fields.name; 13 | return aName === bName ? 0 : aName > bName ? 1 : -1; 14 | }); 15 | 16 | const manifestChild = ({ 17 | manifestType, 18 | target, 19 | repoName, 20 | repoUrl, 21 | ownerPic, 22 | skipVersions = false, 23 | }) => { 24 | // some manifests are arrays, so are not 25 | if (!Array.isArray(target)) target = [target]; 26 | 27 | const children = target.map((g) => { 28 | // various strategies for picking up the description 29 | let name = null; 30 | let list = null; 31 | 32 | if (typeof g === "string") { 33 | name = g.replace("https://www.googleapis.com/auth/", ""); 34 | list = [g]; 35 | } else if (manifestType === "addOns") { 36 | if (g.id) { 37 | name = g.id; 38 | list = [g.id]; 39 | } else { 40 | name = Object.keys(g).join(","); 41 | list = Object.keys(g); 42 | } 43 | } else { 44 | name = g.userSymbol || g.access || g.name; 45 | list = [g.libraryId || g.serviceId || name]; 46 | if (g.version && !skipVersions) 47 | name += `.v${g.version}`.replace(".vv", ".v"); 48 | } 49 | 50 | return { 51 | name, 52 | // empty children will mark the end of the tree for d3 53 | children: [], 54 | entry: g, 55 | list, 56 | type: "entries", 57 | manifestType, 58 | repoName, 59 | repoUrl, 60 | ownerPic, 61 | }; 62 | }); 63 | 64 | const pack = { 65 | name: manifestType, 66 | children, 67 | target, 68 | type: manifestType, 69 | repoName, 70 | repoUrl, 71 | ownerPic, 72 | }; 73 | 74 | return pack; 75 | }; 76 | 77 | const makeManifestChildren = ({ 78 | mf, 79 | id, 80 | repoName, 81 | ownerPic, 82 | vTypes, 83 | repoUrl, 84 | }) => { 85 | const manifest = mf.manifests.get(id); 86 | 87 | const m = vTypes 88 | .map((n) => { 89 | // eg libraries 90 | const f = n.name; 91 | // the values in the manifest for this type 92 | let target = manifest[f] || []; 93 | 94 | return manifestChild({ 95 | manifestType: f, 96 | target, 97 | repoName, 98 | 99 | ownerPic, 100 | repoUrl, 101 | }); 102 | }) 103 | .filter((f) => f.children.length || f.childrenCount); 104 | 105 | return m; 106 | }; 107 | 108 | const getDependencies = (mf, type) => { 109 | if (!mf) return null; 110 | const libraries = Array.from(mf.maps[type].values()).sort((a, b) => { 111 | const aName = a.label; 112 | const bName = b.label; 113 | return aName === bName ? 0 : aName > bName ? 1 : -1; 114 | }); 115 | return libraries; 116 | }; 117 | 118 | export const getLibraries = (mf) => getDependencies(mf, "libraries"); 119 | export const getAdvancedServices = (mf) => 120 | getDependencies(mf, "advancedServices"); 121 | export const getAddOns = (mf) => getDependencies(mf, "addOns"); 122 | export const getOauthScopes = (mf) => getDependencies(mf, "oauthScopes"); 123 | export const getRuntimeVersions = (mf) => 124 | getDependencies(mf, "runtimeVersions"); 125 | export const getWebapps = (mf) => getDependencies(mf, "webapps"); 126 | export const getDataStudios = (mf) => getDependencies(mf, "dataStudios"); 127 | export const getTimeZones = (mf) => getDependencies(mf, "timeZones"); 128 | 129 | export const arrangeTreeData = (state) => { 130 | // first job is to do a vanilla tree by owner 131 | const { viewType } = state; 132 | 133 | if (viewType === "owners") { 134 | const ownerTree = makeOwnerTreeData(state); 135 | return ownerTree; 136 | } else { 137 | const viewTree = makeManifestTreeData(state); 138 | return viewTree; 139 | } 140 | }; 141 | 142 | export const makeOwnerTreeData = (state) => { 143 | // the objective is to make tree shaped data for d3 144 | // { name: owner, children: [{ name: repo: children: [{ name: libraries }, { name: advanced services }] }] } 145 | const { gd, dob, fob, vTypes } = state; 146 | 147 | if (!gd || !state.mf) return null; 148 | 149 | // if theyre not interlocked, then the filtering hasnt been completed yet 150 | const mf = state.interlockedFilters ? state.mf : reduceManifests(state); 151 | 152 | // if there are no manifests after interlocking, then there's nothing to do yet 153 | if (!mf) return null; 154 | 155 | // we'll use this to further reduce the owners, which will contain all owners 156 | const reposByOwnerSet = new Set( 157 | fob.repos.allFiltered().map(dob.reposByOwner.accessor) 158 | ); 159 | 160 | const owners = sorter( 161 | fob.owners 162 | .allFiltered() 163 | .filter((d) => reposByOwnerSet.has(dob.owners.accessor(d))) 164 | ); 165 | 166 | const repos = fob.repos.allFiltered(); 167 | const files = fob.files.allFiltered(); 168 | 169 | // this is the kind of tree needed by d3 170 | const getOwnerAndChildren = ({ ownerOb }) => { 171 | const id = dob.owners.accessor(ownerOb); 172 | 173 | const children = repos 174 | .filter((s) => dob.reposByOwner.accessor(s) === id) 175 | .map((repoOb) => { 176 | const id = dob.repos.accessor(repoOb); 177 | const children = state.depth < depths.FILE ? [] : files 178 | .filter((s) => dob.filesByRepo.accessor(s) === id) 179 | .map((fileOb) => { 180 | const id = dob.files.accessor(fileOb); 181 | const manifest = mf.manifests.get(fileOb.fields.sha); 182 | const { path: fp } = fileOb.fields; 183 | return { 184 | ownerPic: ownerOb.fields.avatar_url, 185 | repoName: repoOb.fields.name, 186 | repoUrl: repoOb.fields.url, 187 | name: fp, 188 | manifest, 189 | id, 190 | type: "files", 191 | file: fileOb, 192 | // the children are each of the manifest options 193 | // but we dont need them if only the abbreviated map is being shown 194 | children: state.depth > depths.FILE 195 | ? makeManifestChildren({ 196 | mf, 197 | id: fileOb.fields.sha, 198 | state, 199 | repoName: repoOb.fields.name, 200 | repoUrl: repoOb.fields.url, 201 | ownerPic: ownerOb.fields.avatar_url, 202 | vTypes, 203 | }) 204 | : [], 205 | }; 206 | }); 207 | return { 208 | children, 209 | childrenCount: children.length, 210 | ownerPic: ownerOb.fields.avatar_url, 211 | repoName: repoOb.fields.name, 212 | repoUrl: repoOb.fields.url, 213 | repo: repoOb, 214 | name: repoOb.fields.name, 215 | type: "repos", 216 | }; 217 | }); 218 | 219 | return { 220 | children, 221 | childrenCount: children.length, 222 | owner: ownerOb, 223 | type: "owners", 224 | name: ownerOb.fields.name || ownerOb.fields.login, 225 | id, 226 | }; 227 | }; 228 | 229 | const t = Array.from(owners.values()).reduce( 230 | (p, c) => { 231 | const ownerChildren = getOwnerAndChildren({ ownerOb: c }); 232 | p.children.push(ownerChildren); 233 | return p; 234 | }, 235 | { name: "owners", children: [], type: "root" } 236 | ); 237 | 238 | return t; 239 | }; 240 | 241 | export const makeManifestTreeData = (state) => { 242 | const { gd, viewType, cfManifests, fob, dob, vTypes, filterPlus } = state; 243 | 244 | if (!gd || !state.mf) return null; 245 | 246 | // if theyre not interlocked, then the filtering hasnt been completed yet 247 | const mf = state.interlockedFilters ? state.mf : reduceManifests(state); 248 | 249 | // if there are no manifests after interlocking, then there's nothing to do yet 250 | if (!mf) return null; 251 | 252 | // use this as the base manifest type 253 | const base = cfManifests.allFiltered(); 254 | 255 | // a map will make this easier to reference later 256 | const shaMap = fob.files.allFiltered().reduce((p, c) => { 257 | const sha = dob.filesBySha.accessor(c); 258 | if (!p.has(sha)) 259 | p.set(sha, { 260 | files: [], 261 | sha, 262 | }); 263 | p.get(sha).files.push(c); 264 | return p; 265 | }, new Map()); 266 | 267 | const { idAccessor, filterName, objectKeys } = vTypes.find( 268 | (f) => viewType === f.name 269 | ); 270 | const filterOb = state[filterName]; 271 | 272 | // now create a tree with that as the base 273 | 274 | const t = base 275 | .filter( 276 | (f) => 277 | f[viewType] && 278 | ((typeof f[viewType] === "object" && Object.keys(f[viewType]).length) || 279 | f[viewType].length) 280 | ) 281 | .reduce((p, viewItem) => { 282 | // this will give something like the array of libraries in this manifest 283 | let a = objectKeys 284 | ? Object.keys(viewItem[viewType]).map((k) => { 285 | return { 286 | ...viewItem[viewType][k], 287 | id: k, 288 | }; 289 | }) 290 | : Array.isArray(viewItem[viewType]) 291 | ? viewItem[viewType] 292 | : [viewItem[viewType]]; 293 | 294 | a.forEach((g) => { 295 | // each viewtype has a different style of id 296 | const id = idAccessor ? g[idAccessor] : g; 297 | if (!id) { 298 | console.log(idAccessor, viewType, g); 299 | throw new Error("couldnt find id for", idAccessor, g); 300 | } 301 | // this is organizing the qhole structure by selected viewtype 302 | // need to dump the parts of the manifest that are not required 303 | // because they weren't part of the filtering 304 | // TODO - there might be a good option for leaving them in 305 | // that would should related libraries for example as well as selected ones 306 | // by default we'll tkae them out 307 | 308 | if (!filterPlus || !filterOb.length || filterOb.indexOf(id) !== -1) { 309 | if (!p.has(id)) { 310 | p.set(id, { 311 | items: [], 312 | id, 313 | item: g, 314 | }); 315 | } 316 | // just pile it all on - we'll need all this to sort it out later 317 | p.get(id).items.push({ 318 | variant: g, 319 | viewItem, 320 | }); 321 | } 322 | }); 323 | 324 | return p; 325 | }, new Map()); 326 | 327 | // now we have the whole thing arranged by viewtype 328 | // need to create a tree based on that and dispose of irrelevant things 329 | const viewFiles = Array.from(t.values()).map((value) => { 330 | // borrow the code for making a child from the regular owner shaped tree 331 | // it'll only return 1 child 332 | const cl = manifestChild({ 333 | manifestType: viewType, 334 | target: value.item, 335 | skipVersions: true, 336 | }).children; 337 | if (cl.length !== 1) { 338 | throw new Error("should have returned a single child for ", value.item); 339 | } 340 | const cld = cl[0]; 341 | 342 | return { 343 | name: cld.name, 344 | // TODO maybe the versions should be updated using the variants and the names too 345 | entry: cld.entry, 346 | list: cld.list, 347 | manifestType: cld.manifestType, 348 | type: cld.type, 349 | children: value.items.reduce((p, f) => { 350 | // find all the files that match this sha 351 | 352 | const sha = f.viewItem.id; 353 | const pc = p.concat( 354 | shaMap.get(sha).files.map((file) => { 355 | const repo = gd.repos.get(dob.filesByRepo.accessor(file)); 356 | const owner = gd.owners.get(dob.reposByOwner.accessor(file)); 357 | const id = dob.files.accessor(file); 358 | const manifest = mf.manifests.get(sha); 359 | const { path: fp } = file.fields; 360 | return { 361 | ownerPic: owner.fields.avatar_url, 362 | repoName: repo.fields.name, 363 | repoUrl: repo.fields.url, 364 | name: fp, 365 | manifest, 366 | id, 367 | type: "files", 368 | file, 369 | children: [ 370 | { 371 | ownerPic: owner.fields.avatar_url, 372 | childrenCount: 1, 373 | repoName: repo.fields.name, 374 | repoUrl: repo.fields.url, 375 | repo, 376 | name: repo.fields.name, 377 | type: "repos", 378 | children: [ 379 | { 380 | owner, 381 | childrenCount: 0, 382 | type: "owners", 383 | name: owner.fields.name || owner.fields.login, 384 | id: owner.fields.id, 385 | children: [], 386 | }, 387 | ], 388 | }, 389 | ], 390 | }; 391 | }) 392 | ); 393 | 394 | return pc; 395 | }, []), 396 | }; 397 | }, []); 398 | 399 | return { 400 | name: viewType, 401 | children: viewFiles, 402 | type: "root", 403 | }; 404 | }; 405 | 406 | // this will mash it into the shape needed for a d3 tree structure 407 | export const tree = ({ data, width }) => { 408 | if (!data || !width) return null; 409 | 410 | const root = d3.hierarchy(data); 411 | root.dx = 10; 412 | root.dy = width / (root.height + 1); 413 | return d3.tree().nodeSize([root.dx, root.dy])(root); 414 | }; 415 | 416 | // this is for a summary list of the versions discovered 417 | export const mapVersions = (ot) => { 418 | return (ot || []).map((f) => ({ 419 | library: f, 420 | name: f.label, 421 | id: f.id, 422 | versionNames: `versions:${Array.from(f.versions.values()) 423 | .map((g) => g.version) 424 | .join(",") || f.label}`, 425 | })); 426 | }; 427 | 428 | // TODO : might be nice to provide stats card one day 429 | export const getStats = () => { 430 | // generate a bunch of stats 431 | return {}; 432 | /* 433 | return ["owners", "repos", "shaxs", "files"].reduce((p, c) => { 434 | p[c] = { 435 | list: getGdItem(state, c), 436 | count: { 437 | get() { 438 | return this.list.length; 439 | }, 440 | }, 441 | }; 442 | 443 | return p; 444 | }, {}); 445 | */ 446 | }; 447 | -------------------------------------------------------------------------------- /src/js/storeinitial.js: -------------------------------------------------------------------------------- 1 | /*global gapi*/ 2 | // this is the vuex definition 3 | // pretty much everything that happens anywhere comes through here 4 | import { gasVizzyInit, delay } from "./gasvizzy"; 5 | import { 6 | logEvent, 7 | signin, 8 | signinGithub, 9 | signout, 10 | gapiInit, 11 | getPickerKey, 12 | getProjectId, 13 | gapiCheckScopes, 14 | gapiAdditionalScopes, 15 | } from "./auth"; 16 | import { applyFilters } from "./filtering"; 17 | import { tree, arrangeTreeData, depths } from "./d3prep"; 18 | import { setTokenData, getTokenData } from "./forager"; 19 | 20 | const vTypes = [ 21 | { 22 | name: "libraries", 23 | idAccessor: "libraryId", 24 | filterName: "libraryFilter", 25 | }, 26 | { 27 | name: "advancedServices", 28 | idAccessor: "serviceId", 29 | filterName: "advancedServiceFilter", 30 | }, 31 | 32 | { 33 | name: "oauthScopes", 34 | idAccessor: "", 35 | filterName: "oauthScopeFilter", 36 | }, 37 | { 38 | name: "addOns", 39 | idAccessor: "id", 40 | filterName: "addOnFilter", 41 | objectKeys: true, 42 | }, 43 | { 44 | name: "runtimeVersion", 45 | idAccessor: "", 46 | filterName: "runtimeVersionFilter", 47 | }, 48 | { 49 | name: "webapp", 50 | idAccessor: "access", 51 | filterName: "webappFilter", 52 | }, 53 | { 54 | name: "dataStudio", 55 | idAccessor: "name", 56 | filterName: "dataStudioFilter", 57 | }, 58 | { 59 | name: "timeZone", 60 | idAccessor: "", 61 | filterName: "timeZoneFilter", 62 | }, 63 | ]; 64 | 65 | const _initial = { 66 | state: { 67 | gettingData: false, 68 | urlParams: null, 69 | resvg: false, 70 | showMessage: null, 71 | showError: false, 72 | spinning: false, 73 | appId: getProjectId(), 74 | pickerKey: getPickerKey(), 75 | isSignedIn: false, 76 | treeModel: [], 77 | pinned: null, 78 | githubToken: null, 79 | showPullDialog: false, 80 | user: null, 81 | gd: null, 82 | mf: null, 83 | cfManifests: null, 84 | // this'll get set to a proper value once the dom is loaded 85 | width: 100, 86 | hireableOwners: false, 87 | ownerFilter: [], 88 | libraryFilter: [], 89 | oauthScopeFilter: [], 90 | advancedServiceFilter: [], 91 | addOnFilter: [], 92 | runtimeVersionFilter: [], 93 | repoFilter: [], 94 | filterPlus: true, 95 | timeZoneFilter: [], 96 | webappFilter: [], 97 | dataStudioFilter: [], 98 | interlockedFilters: true, 99 | dob: null, 100 | fob: null, 101 | root: null, 102 | cacheTimestamp: null, 103 | making: false, 104 | hover: true, 105 | infoData: null, 106 | infoMoused: false, 107 | vizInfo: true, 108 | fobOwners: null, 109 | fobRepos: null, 110 | viewType: "owners", 111 | colors: { 112 | spinner: "amber accent-1", 113 | bigTree: null, 114 | smallTree: null, 115 | info: "pink", 116 | dotChildren: "lime", 117 | dotNoChildren: "pink", 118 | vizTextHovered: "#C2185B", 119 | vizText: "#212121", 120 | gettingData: "red", 121 | making: "orange", 122 | tagChip: "teal accent-4", 123 | }, 124 | vTypes, 125 | depth: depths.REPO, 126 | }, 127 | mutations: { 128 | incrementDepth(state, value) { 129 | state.depth += value; 130 | }, 131 | setDepth(state, value) { 132 | state.depth = value; 133 | }, 134 | setGettingData(state, value) { 135 | state.gettingData = value; 136 | }, 137 | setUrlParams(state, value) { 138 | state.urlParams = value; 139 | }, 140 | setUrlParamsDone(state) { 141 | state.urlParams = { 142 | ...state.urlParams, 143 | doit: false, 144 | }; 145 | }, 146 | setResetsvg(state, value) { 147 | state.resetSvg = value; 148 | }, 149 | setShowError(state, value) { 150 | state.showError = !!value; 151 | state.showMessage = value; 152 | }, 153 | setSpinning(state, value) { 154 | state.spinning = value; 155 | }, 156 | setIsSignedIn(state, value) { 157 | state.isSignedIn = value; 158 | }, 159 | setTreeModel(state, value) { 160 | state.treeModel = value; 161 | }, 162 | 163 | clearTokens(state) { 164 | state.githubToken = null; 165 | }, 166 | setGithubToken(state, value) { 167 | state.githubToken = value; 168 | }, 169 | setPickerApiKey(state, value) { 170 | state.pickerApiKey = value; 171 | }, 172 | flipPullDialog(state) { 173 | state.showPullDialog = !state.showPullDialog; 174 | }, 175 | flipHover(state) { 176 | state.hover = !state.hover; 177 | state.infoMoused = state.hover; 178 | }, 179 | setPullDialog(state, value) { 180 | state.showPullDialog = value; 181 | }, 182 | setUser(state, value) { 183 | state.user = value; 184 | }, 185 | setVizInfo(state, value) { 186 | state.vizInfo = value; 187 | }, 188 | setInfoMoused(state, value) { 189 | state.infoMoused = value; 190 | }, 191 | clearRoot(state) { 192 | state.root = null; 193 | }, 194 | setRoot(state) { 195 | const data = arrangeTreeData(state); 196 | state.root = data ? tree({ data, width: state.width }) : null; 197 | }, 198 | setDob(state, value) { 199 | state.dob = value; 200 | }, 201 | setFob(state, value) { 202 | state.fob = value; 203 | state.fobOwners = value && value.owners && value.owners.allFiltered(); 204 | state.fobRepos = value && value.repos && value.repos.allFiltered(); 205 | }, 206 | setMaking(state, value) { 207 | state.making = value; 208 | }, 209 | setCfManifests(state, mf) { 210 | state.cfManifests = mf; 211 | }, 212 | setMf(state, mf) { 213 | state.mf = mf; 214 | }, 215 | setGd(state, gd) { 216 | state.gd = gd; 217 | }, 218 | setWidth(state, width) { 219 | state.width = width; 220 | }, 221 | setCacheTimestamp(state, value) { 222 | state.cacheTimestamp = value; 223 | }, 224 | setInfoData(state, value) { 225 | state.infoData = value; 226 | }, 227 | _viewType(state, value) { 228 | state.viewType = value; 229 | }, 230 | _interlockedFilters(state, value) { 231 | state.interlockedFilters = value; 232 | }, 233 | _hireableOwners(state, value) { 234 | state.hireableOwners = value; 235 | }, 236 | _ownerFilter(state, value) { 237 | state.ownerFilter = value; 238 | }, 239 | _repoFilter(state, value) { 240 | state.repoFilter = value; 241 | }, 242 | _timeZoneFilter(state, value) { 243 | state.timeZoneFilter = value; 244 | }, 245 | _webappFilter(state, value) { 246 | state.webappFilter = value; 247 | }, 248 | _dataStudioFilter(state, value) { 249 | state.dataStudioFilter = value; 250 | }, 251 | _addOnFilter(state, value) { 252 | state.addOnFilter = value; 253 | }, 254 | _oauthScopeFilter(state, value) { 255 | state.oauthScopeFilter = value; 256 | }, 257 | _advancedServiceFilter(state, value) { 258 | state.advancedServiceFilter = value; 259 | }, 260 | _libraryFilter(state, value) { 261 | state.libraryFilter = value; 262 | }, 263 | _runtimeVersionFilter(state, value) { 264 | state.runtimeVersionFilter = value; 265 | }, 266 | _filterPlus(state, value) { 267 | state.filterPls = value; 268 | }, 269 | setPinned(state, value) { 270 | state.pinned = value; 271 | }, 272 | }, 273 | getters: { 274 | canDeeper(state) { 275 | return state.depth <= depths.FILE; 276 | }, 277 | canShallower(state) { 278 | return state.depth > depths.REPO; 279 | }, 280 | dataColor(state) { 281 | return state.gettingData 282 | ? state.colors.gettingData 283 | : state.making 284 | ? state.colors.making 285 | : "accent"; 286 | }, 287 | checkScopes(state, getters) { 288 | return gapiCheckScopes(getters.isLoggedIn && state.user); 289 | }, 290 | googleToken(state, getters) { 291 | const t = getters.isLoggedIn && state.user.getAuthResponse(true); 292 | return t && t.access_token; 293 | }, 294 | isLoggedIn(state) { 295 | return state.user && state.user.isSignedIn(); 296 | }, 297 | userImage(state, getters) { 298 | return getters.isLoggedIn 299 | ? state.user.getBasicProfile().getImageUrl() 300 | : null; 301 | }, 302 | userName(state, getters) { 303 | return getters.isLoggedIn ? state.user.getBasicProfile().getName() : null; 304 | }, 305 | userEmail(state, getters) { 306 | return getters.isLoggedIn 307 | ? state.user.getBasicProfile().getEmail() 308 | : null; 309 | }, 310 | fobOwners(state) { 311 | return state.fob && state.fob.owners && state.fob.owners.allFiltered(); 312 | }, 313 | leaves(state) { 314 | return (state.root && state.root.leaves().length) || null; 315 | }, 316 | }, 317 | actions: { 318 | goDeeper({ commit, dispatch }) { 319 | commit("incrementDepth", 1); 320 | dispatch("updateRoot"); 321 | }, 322 | 323 | goShallower({ commit, dispatch }) { 324 | commit("incrementDepth", -1); 325 | dispatch("updateRoot"); 326 | }, 327 | signout({ commit }) { 328 | return signout(commit); 329 | }, 330 | signin() { 331 | return signin(); 332 | }, 333 | signinGithub({ commit, dispatch }) { 334 | return signinGithub(commit, dispatch); 335 | }, 336 | setStoredTokens({ state }) { 337 | const { githubToken } = state; 338 | return setTokenData({ githubToken }); 339 | }, 340 | getStoredTokens({ commit }) { 341 | return getTokenData().then((result) => { 342 | if (result) { 343 | const { githubToken } = result; 344 | if (githubToken) commit("setGithubToken", githubToken); 345 | } 346 | }); 347 | }, 348 | vizzyInit({ commit, dispatch }, force) { 349 | commit("setMaking", true); 350 | commit("setGettingData", true); 351 | return gasVizzyInit(force).then(({ gd, mf, timestamp, dob, fob }) => { 352 | commit("setGettingData", false); 353 | commit("setGd", gd); 354 | commit("setMf", mf); 355 | commit("setCacheTimestamp", timestamp); 356 | commit("setDob", dob); 357 | commit("setFob", fob); 358 | dispatch("updateRoot"); 359 | }); 360 | }, 361 | setInterlockedFilters({ dispatch, commit }, value) { 362 | commit("_interlockedFilters", value); 363 | logEvent("filter", { 364 | name: "interlockedFilters", 365 | value, 366 | }); 367 | dispatch("updateRoot"); 368 | }, 369 | setHireableOwners({ dispatch, commit }, value) { 370 | commit("_hireableOwners", value); 371 | logEvent("filter", { 372 | name: "hireableOwners", 373 | value, 374 | }); 375 | dispatch("updateRoot"); 376 | }, 377 | setViewType({ dispatch, commit }, value) { 378 | commit("_viewType", value); 379 | logEvent("filter", { 380 | name: "viewType", 381 | value, 382 | }); 383 | dispatch("updateRoot"); 384 | }, 385 | setOwnerFilter({ dispatch, commit }, value) { 386 | commit("_ownerFilter", value); 387 | logEvent("filter", { 388 | name: "owners", 389 | value, 390 | }); 391 | dispatch("updateRoot"); 392 | }, 393 | setRepoFilter({ dispatch, commit }, value) { 394 | commit("_repoFilter", value); 395 | logEvent("filter", { 396 | name: "repos", 397 | value, 398 | }); 399 | dispatch("updateRoot"); 400 | }, 401 | setTimeZoneFilter({ dispatch, commit }, value) { 402 | commit("_timeZoneFilter", value); 403 | logEvent("filter", { 404 | name: "timeZones", 405 | value, 406 | }); 407 | dispatch("updateRoot"); 408 | }, 409 | setWebappFilter({ dispatch, commit }, value) { 410 | commit("_webappFilter", value); 411 | logEvent("filter", { 412 | name: "webapps", 413 | value, 414 | }); 415 | dispatch("updateRoot"); 416 | }, 417 | setDataStudioFilter({ dispatch, commit }, value) { 418 | commit("_dataStudioFilter", value); 419 | logEvent("filter", { 420 | name: "dataStudios", 421 | value, 422 | }); 423 | dispatch("updateRoot"); 424 | }, 425 | setAddOnFilter({ dispatch, commit }, value) { 426 | commit("_addOnFilter", value); 427 | logEvent("filter", { 428 | name: "addOns", 429 | value, 430 | }); 431 | dispatch("updateRoot"); 432 | }, 433 | setOauthScopeFilter({ dispatch, commit }, value) { 434 | commit("_oauthScopeFilter", value); 435 | logEvent("filter", { 436 | name: "oauthScopes", 437 | value, 438 | }); 439 | dispatch("updateRoot"); 440 | }, 441 | setAdvancedServiceFilter({ dispatch, commit }, value) { 442 | commit("_advancedServiceFilter", value); 443 | logEvent("filter", { 444 | name: "advancedServices", 445 | value, 446 | }); 447 | dispatch("updateRoot"); 448 | }, 449 | setLibraryFilter({ dispatch, commit }, value) { 450 | commit("_libraryFilter", value); 451 | logEvent("filter", { 452 | name: "libraries", 453 | value, 454 | }); 455 | dispatch("updateRoot"); 456 | }, 457 | setRuntimeVersionFilter({ dispatch, commit }, value) { 458 | commit("_runtimeVersionFilter", value); 459 | logEvent("filter", { 460 | name: "runtimeVersions", 461 | value, 462 | }); 463 | dispatch("updateRoot"); 464 | }, 465 | 466 | flipFilterPlus({ dispatch, state, commit }) { 467 | commit("_filterPlus", !state.filterPlus); 468 | logEvent("filter", { 469 | name: "filterPlus", 470 | value: state.filterPlus, 471 | }); 472 | dispatch("updateRoot"); 473 | }, 474 | flipVizInfo({ state, commit }) { 475 | commit("setVizInfo", !state.vizInfo); 476 | logEvent("filter", { 477 | name: "vizInfo", 478 | value: state.vizInfo, 479 | }); 480 | }, 481 | moreScopes({ state }) { 482 | gapiAdditionalScopes(state.user); 483 | }, 484 | gapi({ commit }) { 485 | gapi.load("picker:auth2:client", () => { 486 | // the gapi modules are loaded 487 | // now initialize the auth 488 | gapiInit((user) => { 489 | // record this user 490 | commit("setUser", user); 491 | }).catch((error) => { 492 | console.log("failed to gapiinit", error); 493 | }); 494 | }); 495 | }, 496 | fixParamsLevel({ commit, state }, vp) { 497 | // its possible we're not at a deep enough level for the param being sought 498 | if (vp && vp.type === "manifest" && state.depth < depths.FILE) { 499 | commit("setDepth", depths.FILE); 500 | } 501 | commit("setUrlParams", vp); 502 | }, 503 | 504 | updateRoot({ commit, state }, force) { 505 | // this allows re-render of whatever to show before waiting 506 | // for the length dom update 507 | if (force) { 508 | commit("clearRoot"); 509 | } 510 | 511 | commit("setMaking", true); 512 | commit("setInfoMoused", false); 513 | const { mf, cfManifests } = applyFilters(state); 514 | 515 | commit("setCfManifests", cfManifests); 516 | commit("setFob", state.fob); 517 | commit("setMf", mf); 518 | 519 | return delay(1).then(() => { 520 | commit("setRoot"); 521 | }); 522 | }, 523 | }, 524 | }; 525 | 526 | export default _initial; 527 | --------------------------------------------------------------------------------