├── 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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ repoName }}
8 |
9 |
10 | repo {{ repoName }}
11 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/src/components/statscard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Vizzy stats
4 |
5 | Owners
6 | {{ item.count }}
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ fields.value }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
31 |
47 |
--------------------------------------------------------------------------------
/src/components/runtimeversioncard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ fields.value }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | {{ infoName }}
8 | ({{ infoChildrenCount
10 | }}{{ infoChildrenName }})
12 |
13 |
14 | {{ iconType }} -
16 | {{ infoName || repoName }}
18 |
19 |
20 |
37 |
--------------------------------------------------------------------------------
/src/components/loginchip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mdi-person
7 |
8 |
9 |
10 |
11 | {{ userName }} - click to sign outclick to sign in
12 |
13 |
14 |
38 |
--------------------------------------------------------------------------------
/src/components/webappcard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ fields.access }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{ fields.executeAs }}
19 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ fields.value }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ fields.name }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ fields.serviceId }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{ fields.userSymbol }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ fields.version }}
36 |
37 |
38 |
39 |
40 |
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 |
2 |
3 |
4 |
5 |
6 | {{ errorTitle }}
7 |
8 |
9 |
10 |
11 |
12 | {{ errorMessage }}
13 |
14 |
15 | {{ errorError }}
16 |
17 |
18 |
19 |
20 |
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 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
82 |
--------------------------------------------------------------------------------
/src/components/librarycard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ fields.name }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{ fields.libraryId }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ fields.userSymbol }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ fields.version }}
37 |
38 |
39 |
40 |
41 |
42 |
71 |
87 |
--------------------------------------------------------------------------------
/src/components/libraryfilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
82 |
--------------------------------------------------------------------------------
/src/components/timezonefilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
82 |
--------------------------------------------------------------------------------
/src/components/datastudiofilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
82 |
--------------------------------------------------------------------------------
/src/components/repofilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
32 |
33 |
34 |
35 | {{ data.item.name }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
84 |
--------------------------------------------------------------------------------
/src/components/runtimeversionfilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
84 |
--------------------------------------------------------------------------------
/src/components/advancedservicefilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
84 |
--------------------------------------------------------------------------------
/src/components/addonfilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
88 |
--------------------------------------------------------------------------------
/src/components/datastudiocard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ fields.name }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{
21 | fields.company
22 | }} {{ fields.company }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ fields.shortDescription || fields.description }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Support Url
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {{ fields.feeType.join(",") }}
51 |
52 |
53 |
54 |
55 |
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 |
2 |
22 |
23 |
31 | {{ data.item.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
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 |
2 |
3 |
24 |
25 |
34 |
35 |
36 |
37 | {{ data.item.name }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
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 |
2 |
3 |
4 |
5 | Since this will be a container bound project, you need to pick an
7 | appropriate Drive file to contain this project
9 |
10 |
11 | pick
12 |
13 |
14 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 |
31 |
32 |
33 | {{
34 | row.description || "tag"
35 | }}
36 | {{
38 | row.description || "tag"
39 | }}
40 |
41 |
42 |
43 |
44 | {{ row.tip }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
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 |
2 |
72 |
73 |
131 |
147 |
--------------------------------------------------------------------------------
/src/components/addoncard.vue:
--------------------------------------------------------------------------------
1 |
2 |
75 |
76 |
142 |
158 |
--------------------------------------------------------------------------------
/src/components/repotree copy.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | {{ open ? "mdi-folder-open" : "mdi-folder" }}
15 |
16 |
17 | {{ item.path }} {{ item.skip }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
201 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | View vizzy by
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ leaves }} nodes
18 |
19 |
20 | data is {{ cacheAge }} hrs old
21 |
22 |
23 |
24 |
30 |
36 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Filter Menu
70 |
71 |
72 |
73 |
74 |
75 |
76 | Filters
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Manifest filters
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ fields.login }}
10 | {{
11 | fields.name
12 | }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ hireableText }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ fields.company }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {{ fields.location }}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {{ fields.email }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {{ fields.html_url }}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {{ fields.public_repos }}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {{ fields.followers }}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | @{{ cleanTwitter }}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {{ fields.blog }}
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
120 |
121 |
122 |
124 |
125 | Direct scrviz link to this owner
126 |
128 |
129 |
130 |
131 |
132 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | {{ row.description || row.link }}
152 | {{row.description|| 'extra scrviz profile info'
156 |
157 |
158 | {{ row.tip }}
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
267 |
283 |
--------------------------------------------------------------------------------
/src/components/repotree.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | {{ open ? "mdi-folder-open" : "mdi-folder" }}
14 |
15 |
16 |
17 |
18 | {{ item.path }}
19 |
22 |
23 |
24 |
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 | 
22 |
23 | Detail view with filter and owner info
24 | 
25 |
26 | Filtered view of manifest entry
27 | 
28 |
29 | Filtered view of manifest contents
30 | 
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 |
2 |
3 |
4 |
5 |
6 | Create Script from
8 |
9 |
17 |
18 |
19 |
20 |
21 |
22 | Does the license for
23 |
24 |
29 | permit this usage ?
30 |
31 | (Visit {{ folderLabel }})
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 | Select the files to clone to your new apps script project
49 |
50 | Selected files unsupported by apps script files will be renamed to html.
51 | Folder names will be preserved.
53 |
54 |
58 |
66 |
67 |
68 |
74 |
81 |
82 |
83 | No Repo location url available from github
84 |
85 |
86 |
87 |
88 |
92 |
93 |
94 |
95 | The project will be created in this container
96 |
97 |
98 |
99 | {{ pickedName }}
100 |
101 |
107 |
108 | {{ pickedId }}
109 |
110 |
111 |
112 |
117 |
118 |
119 |
120 | You need to select an appsscript.json at a minimum to create a
122 | project
124 |
125 |
126 | Note: If you get a permission error it's possible the manifest
127 | references a library to which you don't have access.
128 |
129 |
130 | Create
137 |
138 |
146 |
147 |
148 |
154 |
155 |
156 |
157 | Created project {{ modelProjectName }}
158 |
159 | Open in apps Script IDE
160 |
161 |
162 |
163 |
164 | {{ pickedName }}
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
344 |
--------------------------------------------------------------------------------
/src/components/filecard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ fields.repoFullName }}
31 |
32 | {{ folderLabel }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ fields.repoFullName }}
42 |
43 | {{ fileLabel }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{ fields.repoFullName }}
53 |
54 | {{ claspLabel }}
55 |
56 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 | {{ fields.scriptId }}
70 |
71 |
72 |
73 |
74 |
84 |
85 |
86 |
88 |
89 | Open in apps Script IDE (you may need to request access
91 | from)
93 |
95 |
96 |
97 |
98 |
99 |
109 |
110 |
111 |
113 |
114 | Direct scrviz link to this manifest
116 |
118 |
120 |
121 |
122 |
123 |
124 |
Manifest content
125 |
126 |
127 |
132 | Manifest is invalid or empty
133 |
134 |
135 |
136 | Create Apps Script project from github
137 |
138 |
139 |
140 |
141 | You may need to enable the apps script API
143 | for {{ userName }}.
The setting is here
145 | https://script.google.com/home/usersettings
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | {{ userName }} has authorized apps script access
162 | Please grant these additional scopes {{ denied }}
164 | scrviz will need write access your apps script projects
165 |
167 |
168 | {{authButton}}
174 |
175 |
176 |
177 |
178 |
179 |
180 | {{ userName }} has authorized github access
184 | scrviz will need read access your github projects
185 |
187 | authorize
192 |
193 |
194 |
195 |
196 |
197 |
198 | Select repo files and configure Apps Script project
199 |
200 | Configure
208 |
209 |
210 |
211 |
212 |
213 |
351 |
352 |
368 |
--------------------------------------------------------------------------------
/src/components/icons.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | mdi-family-tree
16 | {{
17 | name
18 | }}
19 | mdi-code-json
22 | mdi-tag-multiple
25 | mdi-microsoft-excel
31 | mdi-microsoft-word
34 | mdi-microsoft-windows
40 | mdi-microsoft-office
46 | mdi-beaker-plus
52 | mdi-beaker-minus
58 | mdi-youtube
64 | mdi-linkedin
70 | mdi-information
73 | mdi-filter-off
79 | mdi-open-in-new
82 | mdi-filter
88 | mdi-map-clock
94 | mdi-layers
100 | mdi-file-tree
103 | mdi-office-building
109 | mdi-alert-circle
115 | mdi-file-tree
122 | mdi-map-marker
128 | mdi-email
134 | mdi-briefcase
140 | mdi-file
143 | mdi-toy-brick
149 | mdi-github
155 | mdi-package-variant
161 | mdi-table-eye
167 | mdi-web
173 | mdi-book
179 | mdi-shield
185 | mdi-account-key
191 | mdi-account-convert
194 | mdi-cloud
200 | mdi-close
206 | mdi-chevron-left
212 | mdi-account
218 | mdi-comment
224 | mdi-comment-off
230 | mdi-folder
236 | mdi-refresh
242 | mdi-database
248 | mdi-twitter
254 | mdi-google-maps
257 | mdi-bio
260 | mdi-page-previous
266 | mdi-page-next
272 | mdi-phone
278 | mdi-cash-multiple
281 | mdi-lifebuoy
287 | mdi-text
290 | mdi-currency-usd
296 | mdi-currency-usd-off
302 | mdi-account-group
308 | mdi-counter
314 | mdi-pin
317 | mdi-pin-off
323 | mdi-feather
329 | mdi-blogger
332 | mdi-content-copy
335 | mdi-identifier
338 | mdi-lock-plus
341 | mdi-login
347 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | mdi-language-html5
384 | mdi-semantic-web
390 | mdi-help"
391 |
392 |
393 |
394 | {{ tip }}
395 |
396 |
397 |
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 |
--------------------------------------------------------------------------------