diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..93f1361 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/.editorconfig b/.editorconfig index e9bd6af..bbb2a24 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true + +# Don't mess with stuff in inc. +[app/inc/**] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/.github/workflows/build-deployment-container.yml b/.github/workflows/build-deployment-container.yml new file mode 100644 index 0000000..6f1204e --- /dev/null +++ b/.github/workflows/build-deployment-container.yml @@ -0,0 +1,23 @@ +--- +name: Build deployment container +on: + push: + branches: + - prod + - staging + workflow_dispatch: +jobs: + docker: + runs-on: ubuntu-22.04 + name: Docker Push + steps: + - uses: actions/checkout@v3 + - name: Docker build + run: docker build . -t metacpan/metacpan-explorer:$GITHUB_SHA + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Push build to Docker hub + run: docker push metacpan/metacpan-explorer:$GITHUB_SHA diff --git a/.github/workflows/build-production-container.yml b/.github/workflows/build-production-container.yml new file mode 100644 index 0000000..701c1a6 --- /dev/null +++ b/.github/workflows/build-production-container.yml @@ -0,0 +1,22 @@ +--- +name: Build production container +on: + push: + branches: + - master + workflow_dispatch: +jobs: + docker: + runs-on: ubuntu-22.04 + name: Docker Push + steps: + - uses: actions/checkout@v3 + - name: docker build + run: docker build . -t metacpan/metacpan-explorer:latest + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - name: Push build to Docker Hub + run: docker push metacpan/metacpan-explorer:latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2d490b3 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '25 17 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitmodules b/.gitmodules index 0cffe34..59241ff 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "inc/bootstrap"] path = app/inc/bootstrap - url = https://github.com/twitter/bootstrap.git + url = https://github.com/twbs/bootstrap.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8fa2ff6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:8.15.1-alpine + +EXPOSE 8080 + +WORKDIR /usr/src/app + +# Bundle app source +COPY . . + +RUN npm install --verbose + +CMD [ "npm", "start" ] diff --git a/README.md b/README.md index a6e9678..6701a40 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,33 @@ -metacpan-explorer -================= +# metacpan-explorer -## Updating Submodules +## Clone This repo includes Bootstrap as a submodule, so after cloning (or pulling) make sure your submodule is up to date: - git submodule init && git submodule update +``` +git clone https://github.com/metacpan/metacpan-explorer.git +git submodule init && git submodule update +``` -## Rebuilding the static files +## Docker Development + +###Build an image: + +`docker build -t metacpan/metacpan-explorer .` + +###Run your image: + +`docker run -p 8080:8080 metacpan/metacpan-explorer` + +###View in your browser: + +http://localhost:8080/ + + +## Dockerless Development + +### Rebuilding the static files In the project root run @@ -17,20 +36,25 @@ In the project root run It will install dependencies via npm and regenerate the static files into the `build` directory. -The [developer vm](https://github.com/CPAN-API/metacpan-developer) +The [developer vm](https://github.com/metacpan/metacpan-developer) has everything you need for this. To run it somewhere else you'll need to make sure you have [node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. +### Running the development server + +You can run `node server.js` (or `npm start`) to launch a dev server. +See the comments in `server.js` for additional instructions. + ## Adding Examples -Login using the credentials from https://github.com/CPAN-API/metacpan-credentials/blob/master/github and go to https://gist.github.com/. +Log in using the credentials from https://github.com/metacpan/metacpan-credentials/blob/master/github and go to https://gist.github.com/. Create a new **public** gist with the following file structure: * endpoint.txt -Contains the path to the API endpoint (e.g. `/v0/author/_search`) +Contains the path to the API endpoint (e.g. `/v1/author/_search`) * body.json diff --git a/app/inc/behave.js b/app/inc/behave.js index 9e0f0e2..91b558d 100644 --- a/app/inc/behave.js +++ b/app/inc/behave.js @@ -438,7 +438,7 @@ if(!utils.fenceRange()){ return; } - if (e.keyCode == 13 && e.shiftKey !== true) { + if (e.keyCode == 13 && e.shiftKey !== true) { // NOTE: EDITED utils.preventDefaultEvent(e); utils._callHook('enter:before'); diff --git a/app/index.html b/app/index.html index 68f2b42..ba5cdc8 100644 --- a/app/index.html +++ b/app/index.html @@ -9,7 +9,8 @@ MetaCPAN Explorer - + + diff --git a/app/scripts/model/request.js b/app/scripts/model/request.js index e24fb05..65a7356 100644 --- a/app/scripts/model/request.js +++ b/app/scripts/model/request.js @@ -10,9 +10,9 @@ define(["jquery", "underscore", "model", "store/gist"], function($, _, Model, St getCurl: function() { if(!this.get("endpoint")){ return ""; } var curl = "curl " + (this.get("body") ? "-XPOST " : "") + - "'api.metacpan.org" + this.get("endpoint") + "'"; + "'https://fastapi.metacpan.org/" + this.get("endpoint") + "'"; if(this.get("body")){ - curl += " -d \"$(curl -Ls gist.github.com/metacpan/" + this.id + "/raw/body.json)\""; + curl += " -d \"$(curl -Ls gist.github.com/" + this.store.config.user + "/" + this.id + "/raw/body.json)\""; } return curl; }, @@ -37,7 +37,7 @@ define(["jquery", "underscore", "model", "store/gist"], function($, _, Model, St var self = this; var body = this.get("body"); return $.ajax({ - url: "//api.metacpan.org" + this.get("endpoint"), + url: "https://fastapi.metacpan.org/" + this.get("endpoint"), dataType: "text", type: (body ? "POST" : "GET"), data: (body || null) diff --git a/app/scripts/store/gist.js b/app/scripts/store/gist.js index c6ee0bb..8e4a904 100644 --- a/app/scripts/store/gist.js +++ b/app/scripts/store/gist.js @@ -2,8 +2,12 @@ define(["jquery", "underscore", "backbone"], function($, _, Backbone) { var Storage = function() { return; }; _.extend(Storage.prototype, Backbone.Events, { + config: { + user: 'metacpan-user', + token: github_token() + }, request: function(options) { - options.url += "?access_token=62ac27f3962648d0e158992cbaa3cc3bd8562995"; + //options.url += "?access_token=" + this.config.token; return $.ajax(options); }, find: function(model, options) { @@ -15,7 +19,7 @@ define(["jquery", "underscore", "backbone"], function($, _, Backbone) { }, findAll: function() { return $.ajax({ - url: "https://api.github.com/users/metacpan/gists", + url: "https://api.github.com/users/" + this.config.user + "/gists", context: this, dataType: "json" }); diff --git a/app/scripts/template/sidebar.htm b/app/scripts/template/sidebar.htm index d9946d1..59f0fc2 100644 --- a/app/scripts/template/sidebar.htm +++ b/app/scripts/template/sidebar.htm @@ -11,7 +11,7 @@ -
  • API Documentation
  • -
  • Report Issue
  • -
  • Fork Me
  • +
  • API Documentation
  • +
  • Report Issue
  • +
  • Fork Me
  • diff --git a/app/scripts/view/navbar.js b/app/scripts/view/navbar.js index 4be15d1..72c36b5 100644 --- a/app/scripts/view/navbar.js +++ b/app/scripts/view/navbar.js @@ -55,13 +55,13 @@ define([ View.prototype.render.call(this, { model: model ? model.toJSON() : {} }); this.$endpoint = this.$("input").typeahead({ source: [ - "/v0/file", - "/v0/author", - "/v0/release", - "/v0/distribution", - "/v0/module", - "/v0/favorite", - "/v0/rating" + "/v1/file", + "/v1/author", + "/v1/release", + "/v1/distribution", + "/v1/module", + "/v1/favorite", + "/v1/rating" ] }); this.$("button").tooltip({ placement: "bottom", trigger: "hover", container: "body" }); diff --git a/app/scripts/view/request.js b/app/scripts/view/request.js index 3fc9ac1..2be8b44 100644 --- a/app/scripts/view/request.js +++ b/app/scripts/view/request.js @@ -12,9 +12,15 @@ define([ return View.extend({ name: "request", template: template, + events: { "keydown textarea": function(e) { // Shift + Enter to send request. + // NOTE: Our copy of behave has an edit on line 441 to disable behave's + // enterKey functionality when shift is pressed. + // Behave offers hooks for keydown as well as enter:before but they both + // fire after behave's enterKey function has started, so I think it's + // too late... I think we're stuck with the edit. if((e.keyCode || e.which) === 13 && e.shiftKey === true) { this.model.request(); return false; diff --git a/app/scripts/view/settings.js b/app/scripts/view/settings.js index 0a647aa..bab360f 100644 --- a/app/scripts/view/settings.js +++ b/app/scripts/view/settings.js @@ -16,7 +16,7 @@ define([ item: function() { return [ '
  • ' ].join(''); } diff --git a/bin/docker-build.sh b/bin/docker-build.sh new file mode 100755 index 0000000..ba57636 --- /dev/null +++ b/bin/docker-build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -eux + +docker build -t metacpan/metacpan-explorer:latest . diff --git a/bin/docker-run.sh b/bin/docker-run.sh new file mode 100755 index 0000000..89e73ae --- /dev/null +++ b/bin/docker-run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -eux + +docker run --rm -it -v "$PWD:/usr/src/app" -p 8080:8080 metacpan/metacpan-explorer:latest diff --git a/build.sh b/build.sh index ab9b5b4..8d7de0b 100755 --- a/build.sh +++ b/build.sh @@ -3,7 +3,4 @@ npm install export PATH=`npm bin || echo node_modules/.bin`:$PATH -r.js -o app.build.js -lessc --clean-css app/styles/main.less > build/styles.css - -node env-filter.js < app/index.html > build/index.html +npm run build diff --git a/build/app.js b/build/app.js index a6fd718..b3cbbde 100644 --- a/build/app.js +++ b/build/app.js @@ -122,4 +122,9 @@ * limitations under the License. * ========================================================= */ -(function(){var requirejs,require,define;(function(e){function c(e,t){return f.call(e,t)}function h(e,t){var n,r,i,s,o,a,f,l,c,h,p=t&&t.split("/"),d=u.map,v=d&&d["*"]||{};if(e&&e.charAt(0)===".")if(t){p=p.slice(0,p.length-1),e=p.concat(e.split("/"));for(l=0;l0&&(e.splice(l-1,2),l-=2)}}e=e.join("/")}else e.indexOf("./")===0&&(e=e.substring(2));if((p||v)&&d){n=e.split("/");for(l=n.length;l>0;l-=1){r=n.slice(0,l).join("/");if(p)for(c=p.length;c>0;c-=1){i=d[p.slice(0,c).join("/")];if(i){i=i[r];if(i){s=i,o=l;break}}}if(s)break;!a&&v&&v[r]&&(a=v[r],f=l)}!s&&a&&(s=a,o=f),s&&(n.splice(0,o,s),e=n.join("/"))}return e}function p(t,r){return function(){return n.apply(e,l.call(arguments,0).concat([t,r]))}}function d(e){return function(t){return h(t,e)}}function v(e){return function(t){s[e]=t}}function m(n){if(c(o,n)){var r=o[n];delete o[n],a[n]=!0,t.apply(e,r)}if(!c(s,n)&&!c(a,n))throw new Error("No "+n);return s[n]}function g(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function y(e){return function(){return u&&u.config&&u.config[e]||{}}}var t,n,r,i,s={},o={},u={},a={},f=Object.prototype.hasOwnProperty,l=[].slice;r=function(e,t){var n,r=g(e),i=r[0];return e=r[1],i&&(i=h(i,t),n=m(i)),i?n&&n.normalize?e=n.normalize(e,d(t)):e=h(e,t):(e=h(e,t),r=g(e),i=r[0],e=r[1],i&&(n=m(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},i={require:function(e){return p(e)},exports:function(e){var t=s[e];return typeof t!="undefined"?t:s[e]={}},module:function(e){return{id:e,uri:"",exports:s[e],config:y(e)}}},t=function(t,n,u,f){var l,h,d,g,y,b=[],w;f=f||t;if(typeof u=="function"){n=!n.length&&u.length?["require","exports","module"]:n;for(y=0;y0&&t-1 in e)}function B(e){var t=H[e]={};return b.each(e.match(E)||[],function(e,n){t[n]=!0}),t}function I(e,n,r,i){if(b.acceptData(e)){var s,o,u=b.expando,a="string"==typeof n,f=e.nodeType,c=f?b.cache:e,h=f?e[u]:e[u]&&u;if(h&&c[h]&&(i||c[h].data)||!a||r!==t)return h||(f?e[u]=h=l.pop()||b.guid++:h=u),c[h]||(c[h]={},f||(c[h].toJSON=b.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[h]=b.extend(c[h],n):c[h].data=b.extend(c[h].data,n)),s=c[h],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[b.camelCase(n)]=r),a?(o=s[n],null==o&&(o=s[b.camelCase(n)])):o=s,o}}function q(e,t,n){if(b.acceptData(e)){var r,i,s,o=e.nodeType,u=o?b.cache:e,a=o?e[b.expando]:b.expando;if(u[a]){if(t&&(s=n?u[a]:u[a].data)){b.isArray(t)?t=t.concat(b.map(t,b.camelCase)):t in s?t=[t]:(t=b.camelCase(t),t=t in s?[t]:t.split(" "));for(r=0,i=t.length;i>r;r++)delete s[t[r]];if(!(n?U:b.isEmptyObject)(s))return}(n||(delete u[a].data,U(u[a])))&&(o?b.cleanData([e],!0):b.support.deleteExpando||u!=u.window?delete u[a]:u[a]=null)}}}function R(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(F,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:j.test(r)?b.parseJSON(r):r}catch(s){}b.data(e,n,r)}else r=t}return r}function U(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function it(){return!0}function st(){return!1}function ct(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function ht(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return b.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=b.grep(e,function(e){return 1===e.nodeType});if(at.test(t))return b.filter(t,r,!n);t=b.filter(t,r)}return b.grep(e,function(e){return b.inArray(e,t)>=0===n})}function pt(e){var t=dt.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Mt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function _t(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function Dt(e){var t=Ct.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Pt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval",!t||b._data(t[r],"globalEval"))}function Ht(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,s=b._data(e),o=b._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;i>r;r++)b.event.add(t,n,u[n][r])}o.data&&(o.data=b.extend({},o.data))}}function Bt(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!b.support.noCloneEvent&&t[b.expando]){i=b._data(t);for(r in i.events)b.removeEvent(t,r,i.handle);t.removeAttribute(b.expando)}"script"===n&&t.text!==e.text?(_t(t).text=e.text,Dt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),b.support.html5Clone&&e.innerHTML&&!b.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&xt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function jt(e,n){var r,s,o=0,u=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!u)for(u=[],r=e.childNodes||e;null!=(s=r[o]);o++)!n||b.nodeName(s,n)?u.push(s):b.merge(u,jt(s,n));return n===t||n&&b.nodeName(e,n)?b.merge([e],u):u}function Ft(e){xt.test(e.type)&&(e.defaultChecked=e.checked)}function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,s=[],o=0,u=e.length;for(;u>o;o++)r=e[o],r.style&&(s[o]=b._data(r,"olddisplay"),n=r.style.display,t?(s[o]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(s[o]=b._data(r,"olddisplay",an(r.nodeName)))):s[o]||(i=nn(r),(n&&"none"!==n||!i)&&b._data(r,"olddisplay",i?n:b.css(r,"display"))));for(o=0;u>o;o++)r=e[o],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?s[o]||"":"none"));return e}function sn(e,t,n){var r=$t.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function on(e,t,n,r,i){var s=n===(r?"border":"content")?4:"width"===t?1:0,o=0;for(;4>s;s+=2)"margin"===n&&(o+=b.css(e,n+Zt[s],!0,i)),r?("content"===n&&(o-=b.css(e,"padding"+Zt[s],!0,i)),"margin"!==n&&(o-=b.css(e,"border"+Zt[s]+"Width",!0,i))):(o+=b.css(e,"padding"+Zt[s],!0,i),"padding"!==n&&(o+=b.css(e,"border"+Zt[s]+"Width",!0,i)));return o}function un(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,s=qt(e),o=b.support.boxSizing&&"border-box"===b.css(e,"boxSizing",!1,s);if(0>=i||null==i){if(i=Rt(e,t,s),(0>i||null==i)&&(i=e.style[t]),Jt.test(i))return i;r=o&&(b.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+on(e,t,n||(o?"border":"content"),r,s)+"px"}function an(e){var t=s,n=Qt[e];return n||(n=fn(e,t),"none"!==n&&n||(It=(It||b("