├── .github └── ISSUE_TEMPLATE │ ├── config.ymal │ ├── feature_request.md │ └── bug_report.md ├── ui ├── images │ ├── favicon.png │ ├── background.png │ ├── kibble-logo.png │ ├── kibble-favicon.png │ ├── background-bright.png │ ├── kibble-logo-white.png │ └── sourcetypes │ │ ├── git.png │ │ ├── irc.png │ │ ├── jira.png │ │ ├── mail.png │ │ ├── stats.png │ │ ├── svn.png │ │ ├── gerrit.png │ │ ├── github.png │ │ ├── jenkins.png │ │ ├── travis.png │ │ ├── twitter.png │ │ ├── bugzilla.png │ │ ├── buildbot.png │ │ ├── discourse.png │ │ ├── pipermail.png │ │ └── ponymail.png ├── js │ └── coffee │ │ ├── combine.sh │ │ ├── widget_jsondump.coffee │ │ ├── widget_paragraph.coffee │ │ ├── error_modal.coffee │ │ ├── charts_gauge.coffee │ │ ├── charts_donut.coffee │ │ ├── widget_punchcard.coffee │ │ ├── widget_relation.coffee │ │ ├── phonebook.coffee │ │ ├── widget_factors.coffee │ │ ├── widget_trend.coffee │ │ ├── widget_map.coffee │ │ ├── widget_donut.coffee │ │ ├── widget_publisher.coffee │ │ ├── kibble_account.coffee │ │ ├── widget_mvp.coffee │ │ ├── widget_radar.coffee │ │ ├── widget_preferences.coffee │ │ ├── colors.coffee │ │ ├── widget_admin.coffee │ │ ├── charts_punchcard.coffee │ │ ├── widget_treemap.coffee │ │ ├── widget_top5.coffee │ │ ├── widget_bio.coffee │ │ ├── account.coffee │ │ ├── charts_linechart.coffee │ │ └── widget_messages.coffee ├── css │ ├── chosen-sprite@2x.png │ └── theme.css ├── vendors │ └── font-awesome │ │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── index.html ├── contributors.html ├── dashboard.html ├── engagement.html ├── organisations.html └── relationships.html ├── setup ├── requirements.txt ├── kibble.yaml.sample ├── makeaccount.py └── mappings.json ├── docs ├── source │ ├── _static │ │ └── images │ │ │ ├── kibble-logo.png │ │ │ ├── kibble-architecture.png │ │ │ └── kibble-architecture.puml │ ├── index.rst │ ├── usecases.rst │ └── managing.rst └── Makefile ├── api ├── yaml │ ├── openapi │ │ └── components │ │ │ ├── schemas │ │ │ ├── Sloc.yaml │ │ │ ├── Biography.yaml │ │ │ ├── Trend.yaml │ │ │ ├── CommitterList.yaml │ │ │ ├── SourceID.yaml │ │ │ ├── SourceTypes.yaml │ │ │ ├── UserName.yaml │ │ │ ├── TopList.yaml │ │ │ ├── WidgetDesign.yaml │ │ │ ├── WidgetRow.yaml │ │ │ ├── editView.yaml │ │ │ ├── ActionCompleted.yaml │ │ │ ├── Factor.yaml │ │ │ ├── Empty.yaml │ │ │ ├── Timeseries.yaml │ │ │ ├── SourceType.yaml │ │ │ ├── PhraseList.yaml │ │ │ ├── UserCredentials.yaml │ │ │ ├── Phrase.yaml │ │ │ ├── Error.yaml │ │ │ ├── SourceList.yaml │ │ │ ├── TimeseriesObject.yaml │ │ │ ├── OrgMembers.yaml │ │ │ ├── NewOrg.yaml │ │ │ ├── UserAccount.yaml │ │ │ ├── ViewList.yaml │ │ │ ├── View.yaml │ │ │ ├── UserAccountEdit.yaml │ │ │ ├── UserData.yaml │ │ │ ├── Organisation.yaml │ │ │ ├── WidgetApp.yaml │ │ │ ├── SourceListAdd.yaml │ │ │ ├── Source.yaml │ │ │ └── defaultWidgetArgs.yaml │ │ │ └── securitySchemes │ │ │ └── cookieAuth.yaml │ └── sourcetypes.yaml ├── plugins │ └── __init__.py └── pages │ ├── __init__.py │ ├── filters.py │ ├── widgets.py │ ├── org │ └── sourcetypes.py │ ├── verify.py │ ├── code │ ├── sloc.py │ └── top-sloc.py │ ├── mail │ ├── top-topics.py │ └── keyphrases.py │ ├── issue │ ├── age.py │ └── top.py │ └── forum │ └── top.py ├── CODE_OF_CONDUCT.md ├── BUILDING.md ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── .asf.yaml ├── README.md └── .gitignore /.github/ISSUE_TEMPLATE/config.ymal: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /ui/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/favicon.png -------------------------------------------------------------------------------- /ui/js/coffee/combine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | coffee -b --join ../kibble.v1.js -c *.coffee 3 | 4 | -------------------------------------------------------------------------------- /ui/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/background.png -------------------------------------------------------------------------------- /ui/images/kibble-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/kibble-logo.png -------------------------------------------------------------------------------- /ui/css/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/css/chosen-sprite@2x.png -------------------------------------------------------------------------------- /ui/images/kibble-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/kibble-favicon.png -------------------------------------------------------------------------------- /ui/images/background-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/background-bright.png -------------------------------------------------------------------------------- /ui/images/kibble-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/kibble-logo-white.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/git.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/irc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/irc.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/jira.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/jira.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/mail.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/stats.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/svn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/svn.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/gerrit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/gerrit.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/github.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/jenkins.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/travis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/travis.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/twitter.png -------------------------------------------------------------------------------- /setup/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | pyyaml 3 | bcrypt 4 | elasticsearch 5 | pre-commit 6 | python-dateutil 7 | ndicts 8 | -------------------------------------------------------------------------------- /ui/images/sourcetypes/bugzilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/bugzilla.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/buildbot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/buildbot.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/discourse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/discourse.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/pipermail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/pipermail.png -------------------------------------------------------------------------------- /ui/images/sourcetypes/ponymail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/images/sourcetypes/ponymail.png -------------------------------------------------------------------------------- /docs/source/_static/images/kibble-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/docs/source/_static/images/kibble-logo.png -------------------------------------------------------------------------------- /ui/vendors/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/vendors/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /docs/source/_static/images/kibble-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/docs/source/_static/images/kibble-architecture.png -------------------------------------------------------------------------------- /ui/vendors/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/vendors/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /ui/vendors/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/vendors/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /ui/vendors/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/vendors/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /ui/vendors/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/kibble-1/HEAD/ui/vendors/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /ui/js/coffee/widget_jsondump.coffee: -------------------------------------------------------------------------------- 1 | jsondump = (json, state) -> 2 | pre = new HTML('pre', { style: { whiteSpace: 'pre-wrap'}}) 3 | pre.inject(JSON.stringify(json, null, 2)) 4 | state.widget.inject(pre, true) 5 | 6 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Sloc.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Sloc # 3 | ######################################################################## 4 | properties: 5 | count: 6 | type: object 7 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/securitySchemes/cookieAuth.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # cookieAuth # 3 | ######################################################################## 4 | in: cookie 5 | name: kibble_session 6 | type: apiKey 7 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Biography.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Biography # 3 | ######################################################################## 4 | properties: 5 | foo: 6 | description: TO-DO! 7 | type: string 8 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Trend.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Trend # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | trends: 8 | type: object 9 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/CommitterList.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # CommitterList # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | people: 8 | type: object 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The Apache Kibble project follows the 4 | [Apache Software Foundation code of conduct](https://www.apache.org/foundation/policies/conduct.html). 5 | 6 | If you observe behavior that violates those rules please follow the 7 | [ASF reporting guidelines](https://www.apache.org/foundation/policies/conduct#reporting-guidelines). 8 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/SourceID.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # SourceID # 3 | ######################################################################## 4 | properties: 5 | id: 6 | description: source ID 7 | type: string 8 | required: 9 | - id 10 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/SourceTypes.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # SourceTypes # 3 | ######################################################################## 4 | properties: 5 | git: 6 | schema: 7 | $ref: '#/components/schemas/SourceType' 8 | type: object 9 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/UserName.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # UserName # 3 | ######################################################################## 4 | properties: 5 | username: 6 | example: kibbleguest 7 | type: string 8 | required: 9 | - username 10 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/TopList.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # TopList # 3 | ######################################################################## 4 | properties: 5 | title: 6 | type: string 7 | top: 8 | items: 9 | type: object 10 | type: array 11 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/WidgetDesign.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # WidgetDesign # 3 | ######################################################################## 4 | properties: 5 | rows: 6 | items: 7 | $ref: '#/components/schemas/WidgetRow' 8 | type: array 9 | title: 10 | type: string 11 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/WidgetRow.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # WidgetRow # 3 | ######################################################################## 4 | properties: 5 | children: 6 | items: 7 | $ref: '#/components/schemas/WidgetApp' 8 | type: array 9 | name: 10 | type: string 11 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/editView.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # editView # 3 | ######################################################################## 4 | properties: 5 | id: 6 | type: string 7 | name: 8 | type: string 9 | public: 10 | type: boolean 11 | sources: 12 | type: array 13 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/ActionCompleted.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # ActionCompleted # 3 | ######################################################################## 4 | properties: 5 | message: 6 | description: Acknowledgement message 7 | example: Action completed 8 | type: string 9 | required: 10 | - message 11 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Factor.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Factor # 3 | ######################################################################## 4 | properties: 5 | factors: 6 | items: 7 | type: object 8 | type: array 9 | okay: 10 | type: boolean 11 | required: 12 | - okay 13 | - factors 14 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Empty.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Empty # 3 | ######################################################################## 4 | properties: 5 | id: 6 | description: optional object ID 7 | type: string 8 | page: 9 | description: optional page id 10 | type: string 11 | required: [] 12 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building Kibble # 2 | 3 | Kibble needs a few pieces put together before you can live-test changes: 4 | 5 | - cd to `ui/js/coffee/` and run `bash combine.sh` if you changes coffee 6 | - cd to `api/yaml/openapi` and run `python3 combine.py` for API changes, 7 | or your new API endpoints won't be registered in the openapi.yaml. 8 | Do __NOT__ modify openapi.yaml by hand, edit the right schema file or 9 | script comments to set API specs. 10 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Timeseries.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Timeseries # 3 | ######################################################################## 4 | properties: 5 | interval: 6 | type: string 7 | okay: 8 | type: boolean 9 | timeseries: 10 | items: 11 | $ref: '#/components/schemas/TimeseriesObject' 12 | type: array 13 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/SourceType.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # SourceType # 3 | ######################################################################## 4 | properties: 5 | description: 6 | example: Git repositories 7 | type: string 8 | regex: 9 | example: https?://.*/.+\.git 10 | type: string 11 | title: 12 | example: git 13 | type: string 14 | -------------------------------------------------------------------------------- /setup/kibble.yaml.sample: -------------------------------------------------------------------------------- 1 | elasticsearch: 2 | host: localhost 3 | port: 9200 4 | ssl: false 5 | dbname: kibble 6 | 7 | mail: 8 | mailhost: localhost 9 | mailport: 25 10 | sender: Kibble 11 | 12 | accounts: 13 | allowSignup: true 14 | verifyEmail: false 15 | # Example auto-invite setup: 16 | autoInvite: 17 | - 18 | domain: apache.org 19 | organisation: apache 20 | 21 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/PhraseList.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # PhraseList # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | phrases: 8 | description: A list of key phrases 9 | items: 10 | $ref: '#/components/schemas/Phrase' 11 | type: array 12 | required: 13 | - okay 14 | - phrases 15 | 16 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_paragraph.coffee: -------------------------------------------------------------------------------- 1 | # Paragraph widget 2 | paragraph = (json, state) -> 3 | lmain = mk('div') 4 | state.widget.parent.inject(lmain, true) 5 | if json.title 6 | title = mk('h1', {}, json.title) 7 | app(lmain, title) 8 | if json.text 9 | if isArray(json.text) 10 | for p in json.text 11 | para = mk('p', {style:"font-size: 1.2rem;"}, p) 12 | app(lmain, para) 13 | else 14 | app(lmain, mk('p', {style:"font-size: 1.2rem;"}, json.text)) 15 | 16 | 17 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/UserCredentials.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # UserCredentials # 3 | ######################################################################## 4 | properties: 5 | email: 6 | description: Username (email) 7 | example: guest@kibble.live 8 | type: string 9 | password: 10 | description: User password 11 | example: kibbledemo 12 | type: string 13 | required: 14 | - email 15 | - password 16 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Phrase.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Error # 3 | ######################################################################## 4 | properties: 5 | phrase: 6 | description: The key phrase 7 | example: Ponies are awesome 8 | type: string 9 | count: 10 | description: The number of documents containing this as a key phrase 11 | example: 25 12 | type: integer 13 | required: 14 | - phrase 15 | - count 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Apache Kibble documentation master file, created by 2 | sphinx-quickstart on Thu Jan 11 06:05:51 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Apache Kibble's documentation! 7 | ========================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | setup 14 | managing 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Error.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Error # 3 | ######################################################################## 4 | properties: 5 | code: 6 | description: HTTP Error Code 7 | example: 403 8 | format: int16 9 | type: integer 10 | reason: 11 | description: Human readable error message 12 | example: You need to be logged in to view this endpoint! 13 | type: string 14 | required: 15 | - code 16 | - reason 17 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/SourceList.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # SourceList # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | organisation: 8 | description: The organisation these sources belong to 9 | type: string 10 | sources: 11 | description: The sources in this organisation 12 | items: 13 | $ref: '#/components/schemas/Source' 14 | type: array 15 | required: 16 | - sources 17 | -------------------------------------------------------------------------------- /docs/source/_static/images/kibble-architecture.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | actor user 3 | database elasticsearch 4 | rectangle "Apache Kibble" { 5 | file kibble.yaml 6 | rectangle webserver { 7 | collections "static files" 8 | } 9 | rectangle gunicorn { 10 | rectangle API 11 | } 12 | rectangle API 13 | } 14 | user <-> webserver: (web app) 15 | webserver <--> gunicorn: (reverse proxy) 16 | API <-> elasticsearch 17 | API <~~ kibble.yaml 18 | 19 | rectangle "Apache Kibble Scanners" { 20 | collections scanners 21 | file config.yaml 22 | } 23 | scanners --> elasticsearch 24 | config.yaml ~> scanners 25 | @enduml 26 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/TimeseriesObject.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # TimeseriesObject # 3 | ######################################################################## 4 | properties: 5 | $item: 6 | description: A timeseries object 7 | example: 50 8 | type: integer 9 | $otheritem: 10 | description: A timeseries object 11 | example: 26 12 | type: integer 13 | date: 14 | description: Seconds since UNIX epoch 15 | example: 1508273 16 | type: integer 17 | required: 18 | - date 19 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/OrgMembers.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Organisation Member List # 3 | ######################################################################## 4 | properties: 5 | admins: 6 | description: An array containing the IDs of the admins of this org 7 | type: array 8 | exammple: ['foo@bar', 'bar@foo'] 9 | members: 10 | description: An array containing the IDs of the members of this org 11 | type: array 12 | exammple: ['foo@bar', 'bar@foo'] 13 | required: 14 | - admins 15 | - members 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = ApacheKibble 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/NewOrg.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # NewOrg # 3 | ######################################################################## 4 | properties: 5 | desc: 6 | description: A short description of the organisation 7 | example: Kibble, inc. is awesome 8 | type: string 9 | id: 10 | description: the ID of the organisation, can be anything. 11 | example: demo 12 | type: string 13 | name: 14 | description: The name of the organisation to create 15 | example: Kibble, Inc. 16 | type: string 17 | required: 18 | - id 19 | - name 20 | - desc 21 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/UserAccount.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # UserAccount # 3 | ######################################################################## 4 | properties: 5 | displayname: 6 | description: A display name (e.g. full name) for the account 7 | example: Kibble Guest 8 | type: string 9 | email: 10 | description: Desired username (email address) 11 | example: guest@kibble.live 12 | type: string 13 | password: 14 | description: Desired password for the account 15 | example: kibbledemo 16 | type: string 17 | required: 18 | - email 19 | - password 20 | - displayname 21 | -------------------------------------------------------------------------------- /ui/js/coffee/error_modal.coffee: -------------------------------------------------------------------------------- 1 | 2 | badModal = (str) -> 3 | modalBox = new HTML('div', { class: "errorModal"}) 4 | document.body.appendChild(modalBox) 5 | modalInner = new HTML('div', { class: "errorModalInner" }, str) 6 | modalBox.appendChild(modalInner) 7 | btndiv = new HTML('div', {style: {textAlign: "center", marginTop: "10px"}}, " ") 8 | modalInner.inject(btndiv) 9 | btn = new HTML('button', {class: "btn btn-lg btn-success", onclick:"document.body.removeChild(this.parentNode.parentNode.parentNode);"}, "Gotcha!") 10 | btndiv.inject(btn) 11 | 12 | window.setTimeout(() -> 13 | modalInner.style.visibility = "visible" 14 | modalInner.style.opacity = 1 15 | , 10 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ui/css/theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-admin-template - Free Admin Template Based On Twitter Bootstrap 3.x 3 | * @version 2.4.2 4 | * @license MIT 5 | * @link https://github.com/puikinsh/Bootstrap-Admin-Template 6 | */ 7 | body.boxed { 8 | background: url("../img/pattern/arches.png") repeat; 9 | } 10 | #top > .navbar { 11 | border-top: 3px solid #428bca; 12 | } 13 | #top > .navbar .dropdown-menu > li > a:hover, 14 | #top > .navbar .dropdown-menu > li > a:focus { 15 | background-color: #428bca; 16 | color: #ffffff; 17 | } 18 | #menu { 19 | background-color: #428bca !important; 20 | } 21 | #menu > li > a { 22 | color: #ffffff; 23 | text-shadow: none !important; 24 | } 25 | .sidebar-left-mini #menu > li > a > .link-title { 26 | background-color: #3681c1 !important; 27 | } 28 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/ViewList.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # ViewList # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | organisation: 8 | description: The organisation these views belong to 9 | example: apache 10 | type: string 11 | sources: 12 | description: A list of sources within this org 13 | items: 14 | $ref: '#/components/schemas/Source' 15 | type: array 16 | views: 17 | description: The view available 18 | items: 19 | $ref: '#/components/schemas/View' 20 | type: array 21 | required: 22 | - okay 23 | - organisation 24 | - views 25 | -------------------------------------------------------------------------------- /api/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """ 18 | Kibble API plugins library package 19 | 20 | """ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Idea or feature request 4 | title: '' 5 | labels: 'kind:feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 20 | 21 | **Description** 22 | 23 | 24 | **Use case** 25 | 31 | 32 | **Related Issues** 33 | 34 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/View.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # View # 3 | ######################################################################## 4 | properties: 5 | id: 6 | description: The ID of this view. to be used with other API queries 7 | example: abcdef67126734271 8 | type: string 9 | name: 10 | description: The name of this view 11 | example: Big Data Projects 12 | type: string 13 | organisation: 14 | description: The organisation this view belongs to 15 | example: apache 16 | type: string 17 | public: 18 | description: Whether to make this a global view (admins only!) 19 | type: boolean 20 | sources: 21 | description: The sources within this view 22 | items: 23 | $ref: '#/components/schemas/Source' 24 | type: array 25 | required: 26 | - id 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Kibble # 2 | 3 | ## Community 4 | 5 | The main development and design discussion happens on our mailing lists. 6 | We have a list specifically for development, and one for future user questions and feedback. 7 | 8 | To join in the discussion on the design and roadmap, you can send an email to [dev@kibble.apache.org](mailto:dev@kibble.apache.org).
9 | You can subscribe to the list by sending an email to [dev-subscribe@kibble.apache.org](mailto:dev-subscribe@kibble.apache.org).
10 | You can also browse the archives online at [lists.apache.org](https://lists.apache.org/list.html?dev@kibble.apache.org). 11 | 12 | We also have: 13 | - IRC channel, #kibble on [Freenode](https://webchat.freenode.net/?channels=#kibble) 14 | - Slack channel, #kibble on [ASF slack](https://s.apache.org/slack-invite) 15 | 16 | ## Development installation 17 | 18 | This project requires Python in higher version than 3.3. 19 | More information will come soon! 20 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/UserAccountEdit.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # UserAccountEdit # 3 | ######################################################################## 4 | properties: 5 | defaultOrganisation: 6 | description: If editing an account, this sets the default org (if a member of 7 | it) 8 | example: demo 9 | type: string 10 | displayname: 11 | description: A display name (e.g. full name) for the account 12 | example: Kibble Guest 13 | type: string 14 | email: 15 | description: Desired username (email address) 16 | example: guest@kibble.live 17 | type: string 18 | password: 19 | description: Desired password for the account 20 | example: kibbledemo 21 | type: string 22 | admin: 23 | description: when adding users to orgs, this signifies ownership 24 | type: boolean 25 | example: false 26 | required: 27 | - email 28 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/UserData.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # UserData # 3 | ######################################################################## 4 | properties: 5 | defaultOrganisation: 6 | description: The default organisation. This is where Kibble will collect data 7 | from. 8 | example: apache 9 | type: string 10 | displayName: 11 | description: The display-name of the user 12 | example: Kibble Guest 13 | type: string 14 | organisations: 15 | description: A list of organisations you belong to 16 | example: 17 | - apache 18 | - demo 19 | items: 20 | type: string 21 | type: array 22 | userLevel: 23 | description: User level (user/owner/admin) 24 | example: user 25 | type: string 26 | username: 27 | description: The username (email) of the user 28 | example: guest@kibble.live 29 | type: string 30 | -------------------------------------------------------------------------------- /ui/js/coffee/charts_gauge.coffee: -------------------------------------------------------------------------------- 1 | charts_gaugechart = (obj, data) -> 2 | if data.gauge 3 | data = data.gauge 4 | 5 | config = { 6 | bindto: obj, 7 | data: { 8 | columns: [[data.key or 'value', data.value or data]], 9 | type: 'gauge' 10 | }, 11 | gauge: { 12 | min: 0, 13 | max: 100 14 | }, 15 | color: { 16 | pattern: ['#FF0000', '#F97600', '#F6C600', '#60B044'], 17 | threshold: { 18 | values: [25, 55, 80, 100] 19 | } 20 | }, 21 | tooltip: { 22 | format: { 23 | value: (val) => d3.format(',')(val) 24 | } 25 | } 26 | } 27 | c = c3.generate(config) 28 | return [c, config] 29 | 30 | 31 | gauge = (json, state) -> 32 | 33 | lmain = new HTML('div') 34 | state.widget.inject(lmain, true) 35 | 36 | if json.gauge and json.gauge.text 37 | lmain.inject(new HTML('p', {}, json.gauge.text)) 38 | 39 | gaugeChart = new Chart(lmain, 'gauge', json) 40 | 41 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Organisation.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Organisation # 3 | ######################################################################## 4 | properties: 5 | sourceCount: 6 | description: The number of sources in this organisation 7 | example: 45 8 | type: integer 9 | docCount: 10 | description: The number of source objects gathered from this organisation 11 | example: 973624 12 | type: integer 13 | id: 14 | description: the ID of the organisation, can be anything. 15 | example: demo 16 | type: string 17 | description: 18 | description: A short (optional) description of the organisation 19 | example: demo, inc. is awesome 20 | type: string 21 | name: 22 | description: The name of the organisation to create 23 | example: Kibble, Inc. 24 | type: string 25 | admins: 26 | description: An array containing the IDs of the admins of this org 27 | type: array 28 | exammple: ['foo@bar', 'bar@foo'] 29 | required: 30 | - id 31 | - name 32 | 33 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/WidgetApp.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # WidgetApp # 3 | ######################################################################## 4 | properties: 5 | blocks: 6 | description: Size (width) in UI blocks of the app 7 | example: 4 8 | type: integer 9 | datatype: 10 | description: The top category of this data 11 | example: repo 12 | type: string 13 | name: 14 | description: The title of the widget app 15 | example: Widget Title 16 | type: string 17 | representation: 18 | description: The visual representation style of this widget 19 | example: donut 20 | type: string 21 | source: 22 | description: The API endpoint to get data from 23 | example: code-evolution 24 | type: string 25 | target: 26 | type: string 27 | text: 28 | description: Text to insert into the widget (if paragraph type widget) 29 | type: string 30 | type: 31 | description: The type of widget 32 | example: My Widget 33 | type: string 34 | required: 35 | - type 36 | - name 37 | - blocks 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Problems or issues with Kibble projects 4 | title: '' 5 | labels: 'kind:bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 21 | 22 | **Description:** 23 | 28 | 29 | **Reproduction steps:** 30 | 36 | **Actual result:** 37 | 38 | 39 | **OS:** 40 | 41 | 42 | **Logs:** 43 | 44 | 45 | **Other:** 46 | 49 | -------------------------------------------------------------------------------- /ui/js/coffee/charts_donut.coffee: -------------------------------------------------------------------------------- 1 | charts_donutchart = (obj, data, maxN) -> 2 | a = 0 # Number of segments 3 | asDataArray = [] 4 | if data.counts 5 | data = data.counts 6 | for k, v of data 7 | asDataArray.push([k,v]) 8 | a++ 9 | asDataArray.sort((a,b) => b[1] - a[1]) 10 | if maxN and asDataArray.length > maxN 11 | others = 0 12 | narr = asDataArray.slice(maxN, asDataArray.length-maxN) 13 | asDataArray = asDataArray.slice(0,maxN) 14 | for el in narr 15 | others += el[1] 16 | asDataArray.push(['Others', others]) 17 | asDataArray.sort((a,b) => b[1] - a[1]) 18 | config = { 19 | bindto: obj, 20 | data: { 21 | columns: asDataArray, 22 | type: 'donut' 23 | }, 24 | donut: { 25 | #title: "foo" 26 | width: 50 27 | }, 28 | color: { 29 | pattern: genColors(a + 1, 0.55, 0.475, true) 30 | }, 31 | tooltip: { 32 | format: { 33 | value: (val) => d3.format(',')(val) 34 | } 35 | } 36 | } 37 | c = c3.generate(config) 38 | return [c, config] 39 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_punchcard.coffee: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | punchcard = (json, state) -> 17 | div = document.createElement('div') 18 | if json.text 19 | div.inject(new HTML('p', {}, json.text)) 20 | 21 | state.widget.inject(div, true) 22 | pc = new Chart(div, 'punchcard', json, {punchcard: true}) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/SourceListAdd.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # SourceList # 3 | ######################################################################## 4 | properties: 5 | okay: 6 | type: boolean 7 | organisation: 8 | description: The organisation these sources belong to 9 | type: string 10 | sources: 11 | description: The sources to add 12 | items: 13 | $ref: '#/components/schemas/Source' 14 | type: array 15 | example: 16 | {"sources": [ 17 | { 18 | "sourceURL": "https://github.com/apache/kibble.git", 19 | "type": "github", 20 | "optauth": { 21 | "username": "githubuser", 22 | "password": "githubpass" 23 | } 24 | }, 25 | { 26 | "sourceURL": "https://lists.apache.org/list.html?dev@httpd.apache.org", 27 | "type": "ponymail", 28 | "optauth": { 29 | "cookie": "ponycookie" 30 | } 31 | } 32 | 33 | ] 34 | } 35 | required: 36 | - sources 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | --- 19 | default_stages: [commit, push] 20 | default_language_version: 21 | # force all unspecified python hooks to run python3 22 | python: python3 23 | minimum_pre_commit_version: "1.20.0" 24 | 25 | repos: 26 | - repo: https://github.com/pre-commit/pre-commit-hooks 27 | rev: v2.3.0 28 | hooks: 29 | - id: check-yaml 30 | - id: end-of-file-fixer 31 | - id: trailing-whitespace 32 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/Source.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Source # 3 | ######################################################################## 4 | properties: 5 | cookie: 6 | description: optional session cookie for auth 7 | type: string 8 | organisation: 9 | description: The organisation this source belongs to 10 | type: string 11 | password: 12 | description: optional password for auth 13 | type: string 14 | sourceID: 15 | description: The SourceID of this source 16 | type: string 17 | sourceURL: 18 | description: The origin URL of this source. 19 | type: string 20 | type: 21 | description: The type of source (git, svn, bugzilla, mail, etc) 22 | type: string 23 | username: 24 | description: optional username for auth 25 | type: string 26 | token: 27 | description: API token 28 | type: string 29 | token_secret: 30 | description: API secret token (for some apps) 31 | type: string 32 | consumer_key: 33 | description: consumer key (for some apps) 34 | type: string 35 | consumer_secret: 36 | description: consumer secret (for some apps) 37 | type: string 38 | required: 39 | - type 40 | - sourceURL 41 | -------------------------------------------------------------------------------- /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # https://cwiki.apache.org/confluence/display/INFRA/.asf.yaml+features+for+git+repositories 19 | --- 20 | github: 21 | description: "Apache Kibble - a tool to collect, aggregate and visualize data about any software project" 22 | homepage: https://kibble.apache.org/ 23 | labels: 24 | - kibble 25 | - big-data 26 | - open-source 27 | - python 28 | - visualization 29 | features: 30 | # Enable issues management 31 | issues: true 32 | # Enable wiki for documentation 33 | wiki: false 34 | 35 | enabled_merge_buttons: 36 | squash: true 37 | merge: false 38 | rebase: false 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # This is old version of Apache Kibble 4 | 5 | This repo contains archived code for Kibble v1. The current development for Apache Kibble happens at https://github.com/apache/kibble. 6 | 7 | ## Apache Kibble 8 | 9 | Apache Kibble is a tool to collect, aggregate and visualize data about any software project that uses commonly known tools. It consists of two components: 10 | 11 | - **Kibble Server** (this repository) - main database and UI Server. It serves as the hub 12 | for the scanners to connect to, and provides the overall management of sources as well as the 13 | visualizations and API end points. 14 | - **Kibble scanners** ([kibble-scanners](https://github.com/apache/kibble-scanners)) - a collection of 15 | scanning applications each designed to work with a specific type of resource (git repo, mailing list, 16 | JIRA, etc) and push compiled data objects to the Kibble Server. 17 | 18 | ### Documentation 19 | 20 | For information about the Kibble project and community, visit our 21 | web site at [https://kibble.apache.org/](https://kibble.apache.org/). 22 | 23 | ### Live demo 24 | 25 | If you would love to try Kibble without installing it on your own machine try the online demo of the Kibble 26 | service: [https://demo.kibble.apache.org/](https://demo.kibble.apache.org/). 27 | 28 | 29 | ### Installation 30 | 31 | For installation steps see the [documentation](https://apache-kibble.readthedocs.io/en/latest/setup.html#installing-the-server). 32 | 33 | ### Contributing 34 | 35 | We welcome all contributions that improve the state of the Apache Kibble project. For contribution guidelines 36 | check the [CONTRIBUTING.md](/CONTRIBUTING.md). 37 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_relation.coffee: -------------------------------------------------------------------------------- 1 | relationship = (json, state) -> 2 | div = document.createElement('div') 3 | state.widget.inject(div, true) 4 | chart = new Chart(div, 'relationship', json, {}) 5 | 6 | 7 | id = Math.floor(Math.random() * 987654321).toString(16) 8 | invchk = new HTML('input', { class: "uniform", style: { marginRight: "10px"}, id: "author_#{id}", type: 'checkbox', checked: globArgs.author, name: 'author', value: 'true' }) 9 | 10 | invchk.addEventListener("change", () -> 11 | author = null 12 | if this.checked 13 | author = 'true' 14 | globArgs['author'] = 'true' 15 | 16 | updateWidgets('relationship', null, { author: author }) 17 | ) 18 | invlbl = new HTML('label', { for: "author_#{id}"}, "Inverse map (sender <-> recipient)") 19 | state.widget.inject(invchk) 20 | state.widget.inject(invlbl) 21 | 22 | state.widget.inject(new HTML('br')) 23 | state.widget.inject(new HTML('span', {}, "Minimum signal strength: ")) 24 | sigsel = new HTML('select', {id: "signal_#{id}"}) 25 | for i in [1..5] 26 | opt = new HTML('option', { value: i, selected: if (String(i) == globArgs.links) then "selected" else null}, String(i)) 27 | sigsel.inject(opt) 28 | sigsel.addEventListener("change", () -> 29 | links = null 30 | if this.value 31 | links = this.value 32 | globArgs['links'] = links 33 | 34 | updateWidgets('relationship', null, { links: links }) 35 | ) 36 | state.widget.inject(sigsel) -------------------------------------------------------------------------------- /ui/js/coffee/phonebook.coffee: -------------------------------------------------------------------------------- 1 | 2 | phonebook = (json, state) -> 3 | items = [] 4 | if json.people 5 | id = parseInt(Math.random()*99999999).toString(16) 6 | obj = new HTML('div', { id: id}) 7 | obj.innerText = "Found #{json.people.length} contributors.." 8 | obj.inject(new HTML('br')) 9 | state.widget.inject(obj, true) 10 | 11 | json.people.sort( (a,b) => 12 | if a.name < b.name 13 | return -1 14 | if a.name > b.name 15 | return 1 16 | return 0 17 | ) 18 | 19 | for i, item of json.people 20 | if i > 250 21 | break 22 | idiv = new HTML('div', { class: "phonebook_entry"} ) 23 | left = new HTML('a', { class: "pull-left"}) 24 | if item.gravatar 25 | left.inject(new HTML('img', { class: "img-circle img-reponsive", src: "https://secure.gravatar.com/avatar/#{item.gravatar}.png?d=identicon" ,style: { width: "32px", height: "32px"}})) 26 | right = new HTML('div', { class: "media event"}) 27 | rightInner = new HTML('div', { style: {marginLeft: '10px', width: '280px', height: '24px', display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis'}}) 28 | right.inject(rightInner) 29 | if item.email 30 | title = new HTML('a', { class: "title", href:"contributors.html?page=biography&email=#{item.email}"}, txt(item.name)) 31 | rightInner.inject(title) 32 | rightInner.inject(' - ' + item.contributions + ' contribution' + (if item.contributions != 1 then 's' else '')) 33 | idiv.inject(left) 34 | idiv.inject(right) 35 | obj.inject(idiv) 36 | 37 | 38 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_factors.coffee: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | factors = (json, state) -> 18 | items = [] 19 | if json.factors 20 | id = parseInt(Math.random()*99999999).toString(16) 21 | obj = new HTML('div', { id: id}) 22 | for factor in json.factors 23 | h = new HTML('h1', {}, txt(factor.count.pretty())) 24 | if factor.previous 25 | direction = factor.count - factor.previous 26 | pct = parseInt((direction/factor.previous)* 100) 27 | if direction < 0 28 | h2 = new HTML('span', { style: { marginLeft: "8px", fontSize: "14px", color: 'red'}},[ 29 | new HTML('i', {class: "fa fa-chevron-circle-down"}), 30 | " #{pct}% change since last period" 31 | ]) 32 | h.inject(h2) 33 | else 34 | h2 = new HTML('span', { style: { marginLeft: "8px", fontSize: "14px", color: 'green'}},[ 35 | new HTML('i', {class: "fa fa-chevron-circle-up"}), 36 | " +#{pct}% change since last period" 37 | ]) 38 | h.inject(h2) 39 | 40 | t = txt(factor.title) 41 | obj.inject(new HTML('div', {}, [h,t])) 42 | state.widget.inject(obj, true) -------------------------------------------------------------------------------- /api/pages/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """ 18 | Kibble API scripts library: 19 | 20 | oauth: oauth manager 21 | 22 | """ 23 | 24 | import importlib 25 | import os 26 | # Define all the submodules we have 27 | 28 | rootpath = os.path.dirname(__file__) 29 | print("Reading pages from %s" % rootpath) 30 | 31 | # Import each submodule into a hash called 'handlers' 32 | handlers = {} 33 | 34 | def loadPage(path): 35 | for el in os.listdir(path): 36 | filepath = os.path.join(path, el) 37 | if el.find("__") == -1: 38 | if os.path.isdir(filepath): 39 | loadPage(filepath) 40 | else: 41 | # debugging 42 | # print("Init of filepath %s" % filepath ) 43 | # x = filepath.replace(rootpath, "")[1:] 44 | # print("Result replace of filepath %s type: %s" % (x , type(x)) ) 45 | # explanation: remove rootpath from the beginning and truncate last three characters (=.py) at the end and replace any slash before with a dot 46 | p = filepath.replace(rootpath, "")[1:].replace('/', '.')[:-3] 47 | # windows path 48 | p = p.replace('\\', '.') 49 | # print("Init of module path: pages.%s" % p ) 50 | xp = p.replace('.', '/') 51 | print("Loading endpoint pages.%s as %s" % (p, xp)) 52 | handlers[xp] = importlib.import_module("pages.%s" % p) 53 | 54 | loadPage(rootpath) -------------------------------------------------------------------------------- /api/pages/filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | This is the source list handler for Kibble 20 | """ 21 | 22 | import json 23 | import re 24 | import time 25 | 26 | def run(API, environ, indata, session): 27 | 28 | # We need to be logged in for this! 29 | if not session.user: 30 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 31 | 32 | # Fetch all sources for default org 33 | dOrg = session.user['defaultOrganisation'] or "apache" 34 | res = session.DB.ES.search( 35 | index=session.DB.dbname, 36 | doc_type="view", 37 | size = 5000, 38 | body = { 39 | 'query': { 40 | 'term': { 41 | 'owner': session.user['email'] 42 | } 43 | } 44 | } 45 | ) 46 | 47 | sources = [] 48 | for hit in res['hits']['hits']: 49 | doc = hit['_source'] 50 | if indata.get('quick'): 51 | xdoc = { 52 | 'sourceID': doc['sourceID'], 53 | 'type': doc['type'], 54 | 'sourceURL': doc['sourceURL'] 55 | } 56 | sources.append(xdoc) 57 | else: 58 | sources.append(doc) 59 | 60 | JSON_OUT = { 61 | 'views': sources, 62 | 'okay': True, 63 | 'organisation': dOrg 64 | } 65 | yield json.dumps(JSON_OUT) 66 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_trend.coffee: -------------------------------------------------------------------------------- 1 | 2 | trendBox = (icon, count, title, desc) -> 3 | icons = { 4 | comment: 'fa-comments-o', 5 | down: 'fa-sort-amount-desc', 6 | up: 'fa-sort-amount-asc', 7 | check: 'fa-check-square-o', 8 | caret: 'fa-caret-square-o-right', 9 | spin: 'fa-spin fa-cog' 10 | } 11 | div = document.createElement('div') 12 | div.setAttribute("class", "animated flipInY col-lg-3 col-md-3 col-sm-6 col-xs-12") 13 | cdiv = document.createElement('div') 14 | cdiv.setAttribute("class", "tile-stats") 15 | cdiv.style.width = "100%" 16 | idiv = document.createElement('div') 17 | idiv.setAttribute("class", "icon") 18 | i = document.createElement('i') 19 | i.setAttribute("class", "fa " + (icons[icon] || 'fa-comments-o')) 20 | idiv.appendChild(i) 21 | cdiv.appendChild(idiv) 22 | 23 | # Count 24 | codiv = document.createElement('div') 25 | codiv.setAttribute("class", "count") 26 | codiv.appendChild(document.createTextNode(count)) 27 | cdiv.appendChild(codiv) 28 | 29 | # Title 30 | h3 = document.createElement('h4') 31 | h3.appendChild(document.createTextNode(title)) 32 | cdiv.appendChild(h3) 33 | 34 | # Description 35 | p = document.createElement('p') 36 | p.appendChild(document.createTextNode(desc)) 37 | cdiv.appendChild(p) 38 | 39 | div.appendChild(cdiv) 40 | return div 41 | 42 | trend = (json, state) -> 43 | console.log(state.widget.args.source) 44 | if json.trends # api 2 version 45 | wipe = true 46 | for key, data of json.trends 47 | # Lines changed 48 | linediff = "" 49 | icon = 'up' 50 | if data.before > 0 and data.after > 0 51 | diff = (data.after - data.before) / (data.before || 1) 52 | if diff >= 0 53 | linediff = "Up " + Math.floor(diff*100) + "% since last period" 54 | else 55 | linediff = "Down " + Math.floor(diff*100) + "% since last period" 56 | icon = 'check' 57 | tb = trendBox(icon, data.after.pretty(), data.title, linediff) 58 | state.widget.inject(tb, wipe) 59 | wipe = false 60 | 61 | 62 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_map.coffee: -------------------------------------------------------------------------------- 1 | # Donut widget 2 | worldmap = (json, state) -> 3 | 4 | dt = [] 5 | dtl = [] 6 | l = 0 7 | tot = 0 8 | ttot = 0 9 | top = [] 10 | cmax = 0; 11 | ctotal = 0 12 | if json.countries 13 | for item, details of json.countries 14 | dt.push({name: details.name, value: details.count}) 15 | ctotal += details.count 16 | if details.count > cmax 17 | cmax = details.count 18 | 19 | lmain = document.createElement('div') 20 | radius = ['30%', '50%'] 21 | if not state.widget.div.style.height 22 | lmain.style.height = "500px" 23 | else 24 | lmain.style.height = "100%" 25 | if state.widget.fullscreen 26 | lmain.style.height = "1000px" 27 | radius = ['35%', '60%'] 28 | theme.textStyle.fontSize = 20 29 | lmain.style.width = "100%" 30 | state.widget.inject(lmain, true) 31 | echartMap = echarts.init(lmain, theme); 32 | 33 | echartMap.setOption({ 34 | title: { 35 | text: "Worldwide distribution by country" 36 | subtext: "(" + ctotal.pretty() + " in total from " + (json.numberOfCountries||0) + " countries)" 37 | } 38 | calculable: true, 39 | dataRange: { 40 | min: 0, 41 | max: cmax, 42 | text:['High','Low'], 43 | realtime: false, 44 | calculable : true, 45 | color: ['orangered','yellow','lightskyblue'] 46 | }, 47 | toolbox: { 48 | show: true, 49 | feature: { 50 | dataView : {show: true, title: 'Data view', readOnly: false, lang: ['Data View', 'Close', 'Update']}, 51 | restore: { 52 | show: true, 53 | title: "Restore" 54 | }, 55 | saveAsImage: { 56 | show: true, 57 | title: "Save Image" 58 | } 59 | } 60 | }, 61 | tooltip: { 62 | trigger: 'item', 63 | formatter: (params) -> 64 | return params.seriesName + '
' + params.name + ' : ' + (params.value||0).pretty(); 65 | 66 | }, 67 | 68 | series: [{ 69 | name: state.widget.name, 70 | type: 'map', 71 | mapType: 'world', 72 | mapLocation: { 73 | y : 60 74 | } 75 | itemStyle: { 76 | emphasis:{label:{show:true}} 77 | }, 78 | data: dt 79 | }] 80 | }); 81 | theme.textStyle.fontSize = 12 82 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 |

23 | Sign in 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_donut.coffee: -------------------------------------------------------------------------------- 1 | # Donut widget 2 | donut = (json, state) -> 3 | 4 | dt = [] 5 | dtl = [] 6 | l = 0 7 | tot = 0 8 | ttot = 0 9 | top = [] 10 | if json.counts 11 | dtl = [] 12 | dt = [] 13 | a = 0 14 | for item, count of json.counts 15 | dt.push({name: item, value: count}) 16 | a++ 17 | if not json.alphaSort 18 | dt.sort((a,b) => b.value - a.value) 19 | else 20 | dt.sort((a,b) => if a.name > b.name then 1 else -1) 21 | for item in dt 22 | dtl.push(dt.name) 23 | theme.color = genColors(a+1, 0.55, 0.475, true) #quickColors(a) 24 | 25 | if (state.widget.args.representation == 'commentcount') 26 | code = 0 27 | comment = 0 28 | blank = 0 29 | langs = json.languages 30 | for lang, data of langs 31 | code += data.code 32 | comment += data.comment 33 | blank += data.blank||0 34 | 35 | tot = code + comment 36 | dtl = ['Code', 'Comments'] 37 | dt = [ 38 | {name: 'Code', value: code}, 39 | {name: 'Comments', value: comment} 40 | ] 41 | if blank > 0 42 | dt.push({name: "Blanks", value: blank}) 43 | 44 | theme.color = genColors(3, 0.6, 0.5, true) 45 | 46 | 47 | if (state.widget.args.representation == 'sloccount' or (state.widget.args.representation != 'commentcount' and json.languages)) 48 | langs = json.languages 49 | for lang, data of langs 50 | tot += data.code 51 | top.push(lang) 52 | 53 | top.sort((a,b) => langs[b].code - langs[a].code) 54 | for lang in top 55 | l++ 56 | if (l > 250 || (langs[lang].code/tot) < 0.01) 57 | break 58 | ttot += langs[lang].code 59 | dt.push( { 60 | name: lang, 61 | value: langs[lang].code 62 | }) 63 | dtl.push(lang) 64 | 65 | if (tot != ttot) 66 | dtl.push('Other languages') 67 | dt.push( { 68 | name: 'Other languages', 69 | value: (tot-ttot) 70 | }) 71 | 72 | theme.color = genColors(17, 0.6, 0.5, true) 73 | 74 | data = {} 75 | for el in dt 76 | data[el.name] = el.value 77 | div = new HTML('div') 78 | state.widget.inject(div, true) 79 | chartBox = new Chart(div, 'donut', data, 25) 80 | 81 | -------------------------------------------------------------------------------- /api/pages/widgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/widgets/{pageid} 19 | ######################################################################## 20 | # get: 21 | # summary: Shows the widget layout for a specific page 22 | # security: 23 | # - cookieAuth: [] 24 | # parameters: 25 | # - name: pageid 26 | # in: path 27 | # description: Page ID to fetch design for 28 | # required: true 29 | # schema: 30 | # type: string 31 | # responses: 32 | # '200': 33 | # description: 200 Response 34 | # content: 35 | # application/json: 36 | # schema: 37 | # $ref: '#/components/schemas/WidgetDesign' 38 | # default: 39 | # description: unexpected error 40 | # content: 41 | # application/json: 42 | # schema: 43 | # $ref: '#/components/schemas/Error' 44 | ######################################################################## 45 | """ 46 | This is the widget design handler for Kibble 47 | """ 48 | 49 | import yaml 50 | import json 51 | 52 | def run(API, environ, indata, session): 53 | 54 | if not session.user: 55 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 56 | 57 | widgets = yaml.load(open("yaml/widgets.yaml"), Loader=yaml.Loader) 58 | 59 | page = indata['pageid'] 60 | if not page or page == '0': 61 | page = widgets.get('defaultWidget', 'repos') 62 | if page in widgets['widgets']: 63 | yield json.dumps(widgets['widgets'][page]) 64 | else: 65 | raise API.exception(404, "Widget design not found!") 66 | 67 | -------------------------------------------------------------------------------- /api/pages/org/sourcetypes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/org/sourcetypes 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/SourceTypes' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Lists the available source types supported by Kibble 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/Sloc' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Lists the available source types supported by Kibble 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the source types handler for Kibble 68 | """ 69 | 70 | import yaml 71 | import json 72 | 73 | def run(API, environ, indata, session): 74 | 75 | types = yaml.load(open("yaml/sourcetypes.yaml"), Loader=yaml.Loader) 76 | 77 | yield json.dumps(types) 78 | 79 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_publisher.coffee: -------------------------------------------------------------------------------- 1 | viewJS = {} 2 | publisherWidget = null 3 | 4 | publisherPublic = (json, state) -> 5 | publisher(json, state, true) 6 | 7 | publisher = (json, state, nolink) -> 8 | div = document.createElement('div') 9 | state.public = true 10 | twidget = json.widget.replace(/(-\d?year|-all|-month)/, "") 11 | if twidget in ['repo-relationship', 'issue-relationship', 'mail-relationship'] 12 | relationship(json, state) 13 | if twidget in ['languages', 'compare-commits', 'repo-size', 'repo-commits'] 14 | donut(json, state) 15 | else if twidget in ['evolution', 'evolution-extended', 'commit-history', 'committer-count', 'issue-count','issue-operators', 'commit-lines', 'email-count', 'issue-queue', 'file-age', 'file-creation', 'im-stats', 'log-stats'] 16 | linechart(json, state) 17 | else if twidget in ['log-map'] 18 | worldmap(json, state) 19 | else if twidget in ['sloc-map'] 20 | treemap(json, state) 21 | else if twidget.match(/top/) 22 | top5(json, state) 23 | viewJS = JSON.stringify({ 24 | view: json.eview, 25 | widget: json.widget 26 | } 27 | ) 28 | if not nolink 29 | link = mk('input', { type: "button", class:"btn btn-success", value: "Publish this widget", onclick: "publishWidget();"}) 30 | state.widget.inject(link) 31 | publisherWidget = state.widget 32 | else 33 | if not location.href.match(/snoot\.io/) 34 | link = mk('a', { href: "https://www.snoot.io/", style: "font-size: 10px; margin-left: 60px; font-family: sans-serif;"}, "Data courtesy of Snoot.io") 35 | state.widget.inject(link) 36 | 37 | publishWidget = () -> 38 | postJSON("publish", { 39 | publish: JSON.parse(viewJS) 40 | }, null, postPublishLink) 41 | 42 | postPublishLink = (json, state) -> 43 | if json.id 44 | pdiv = get('publishercode') 45 | if not pdiv 46 | pdiv = mk('pre', {id:"publishercode", style: "padding: 5px; border: 1px dashed #333; background: #FFD;"}) 47 | publisherWidget.inject(pdiv) 48 | pdiv.innerHTML = "" 49 | added = "" 50 | if json.type and json.type.match(/log-map/) 51 | added = "\n\n" 52 | app(pdiv, txt("Script code for publishing:\n\n
\n\n#{added}")) 53 | else 54 | alert("Something broke :(") 55 | -------------------------------------------------------------------------------- /ui/js/coffee/kibble_account.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | kibbleLoginCallback = (json, state) -> 19 | userAccount = json 20 | # Everything okay, redirect to dashboard :) 21 | m = location.href.match(/\?redirect=(.+)$/) 22 | if m and not m[1].match(/:/) 23 | location.href = m[1] 24 | else 25 | location.href = "/organisations.html?page=org" 26 | 27 | kibbleLogin = (email, password) -> 28 | put("session", {email: email, password: password}, null, kibbleLoginCallback) 29 | return false 30 | 31 | signout = () -> 32 | xdelete('session', {}, {}, () -> location.href = 'login.html') 33 | 34 | accountCallback = (json, state) -> 35 | obj = get('signup') 36 | obj.innerHTML = "" 37 | h = new HTML('h3', {}, "Account created!") 38 | obj.appendChild(h) 39 | if json.verified 40 | t = new HTML('p', {}, "You can now log in and use your account") 41 | else 42 | t = new HTML('p', {}, "Please check your email account for a verification email.") 43 | obj.appendChild(t) 44 | 45 | kibbleSignup = (form) -> 46 | email = form.email.value 47 | displayName = form.displayname.value 48 | password = form.password.value 49 | password2 = form.password2.value 50 | 51 | # Passwords must match 52 | if password != password2 53 | alert("Passwords must match!") 54 | return false 55 | 56 | # Username must be >= 2 chars 57 | if displayName.length < 2 58 | alert("Please enter a proper display name!") 59 | return false 60 | 61 | # Email must be valid 62 | if not email.match(/([^@]+@[^.]+\.[^.])/) 63 | alert("Please enter a valid email address!") 64 | return false 65 | 66 | put('account', { 67 | email: email, 68 | password: password, 69 | displayname: displayName 70 | }, null, accountCallback) 71 | 72 | return false -------------------------------------------------------------------------------- /ui/js/coffee/widget_mvp.coffee: -------------------------------------------------------------------------------- 1 | mvp = (json, state) -> 2 | nlist = new HTML('select', { name: 'size', id: 'size'}) 3 | for i in [10,20,50,100,200,500,1000,2000] 4 | el = new HTML('option', { value: i, text: i+""}) 5 | if globArgs.size and parseInt(globArgs.size) == i 6 | el.selected = 'selected' 7 | el.inject(txt(i+"")) 8 | nlist.inject(el) 9 | nlist.addEventListener("change", () -> 10 | n = this.value 11 | if n == "" 12 | n = null 13 | globArgs.size = n 14 | updateWidgets('mvp', null, { size: n }) 15 | 16 | , false) 17 | state.widget.inject( 18 | new HTML('b', {}, "List size: "), 19 | true 20 | ) 21 | state.widget.inject(nlist) 22 | 23 | 24 | nlist = new HTML('select', { name: 'sort', id: 'sort'}) 25 | for i in ['commits', 'issues', 'emails'] 26 | el = new HTML('option', { value: i, text: i}) 27 | if globArgs.sort and globArgs.sort == i 28 | el.selected = 'selected' 29 | el.inject(txt(i)) 30 | nlist.inject(el) 31 | nlist.addEventListener("change", () -> 32 | n = this.value 33 | if n == "" 34 | n = null 35 | globArgs.sort = n 36 | updateWidgets('mvp', null, { sort: n }) 37 | 38 | , false) 39 | state.widget.inject( 40 | new HTML('b', {}, " Sort by: "), 41 | ) 42 | state.widget.inject(nlist) 43 | 44 | tbl = mk('table', {class: "table table-striped"}) 45 | tr = mk('tr', {}, [ 46 | mk('th', {}, "Rank"), 47 | mk('th', {}, "Avatar"), 48 | mk('th', {}, "Name",) 49 | mk('th', {}, "Address"), 50 | mk('th', {}, if globArgs.author then "Authorings" else "Commits"), 51 | mk('th', {}, "Issues"), 52 | mk('th', {}, "Email") 53 | ]) 54 | app(tbl, tr) 55 | tb = new HTML('tbody') 56 | for person, i in json.sorted 57 | tr = mk('tr', {scope: 'row'}, [ 58 | mk('td', {}, (i+1).pretty()), 59 | mk('td', {}, new HTML('img', {style: { width: '32px', height: '32px'}, class: "img-circle img-responsive", src:"https://secure.gravatar.com/avatar/#{person.md5}.png?d=identicon"})), 60 | mk('td', {}, mk('a', { href: "?page=people&email=#{person.address}"}, person.name)), 61 | mk('td', {}, person.address), 62 | mk('td', {}, person.commits.pretty()), 63 | mk('td', {}, person.issues.pretty()), 64 | mk('td', {}, person.emails.pretty()), 65 | ]) 66 | tb.inject(tr) 67 | app(tbl, tb) 68 | state.widget.inject(tbl) 69 | #updateWidgets('trends', null, { email: email }) 70 | 71 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_radar.coffee: -------------------------------------------------------------------------------- 1 | # Donut widget 2 | 3 | radarIndicators = [] 4 | 5 | radar = (json, state) -> 6 | 7 | lmain = new HTML('div') 8 | state.widget.inject(lmain, true) 9 | 10 | radarChart = new Chart(lmain, 'radar', json.radar) 11 | 12 | 13 | # Harmonizer 14 | id = Math.floor(Math.random() * 987654321).toString(16) 15 | chk = document.createElement('input') 16 | chk.setAttribute("type", "checkbox") 17 | chk.setAttribute("id", id) 18 | chk.style.marginLeft = '10px' 19 | if globArgs.harmonize and globArgs.harmonize == 'true' 20 | chk.checked = true 21 | chk.addEventListener("change", () -> 22 | harmonize = null 23 | if this.checked 24 | harmonize = 'true' 25 | globArgs['harmonize'] = 'true' 26 | 27 | updateWidgets('radar', null, { harmonize: harmonize }) 28 | ) 29 | state.widget.inject(mk('br')) 30 | state.widget.inject(chk) 31 | label = document.createElement('label') 32 | label.setAttribute("for", id) 33 | label.setAttribute("title", "Check this box to harmonize edges to organisational averages") 34 | chk.setAttribute("title", "Check this box to harmonize edges to organisational averages") 35 | label.style.paddingLeft = '5px' 36 | label.appendChild(document.createTextNode('Harmonize edges')) 37 | state.widget.inject(label) 38 | 39 | # Relativizer 40 | id = Math.floor(Math.random() * 987654321).toString(16) 41 | chk = document.createElement('input') 42 | chk.setAttribute("type", "checkbox") 43 | chk.setAttribute("id", id) 44 | chk.style.marginLeft = '10px' 45 | if globArgs.relativize and globArgs.relativize == 'true' 46 | chk.checked = true 47 | chk.addEventListener("change", () -> 48 | relativize = null 49 | if this.checked 50 | relativize = 'true' 51 | globArgs['relativize'] = 'true' 52 | 53 | updateWidgets('radar', null, { relativize: relativize }) 54 | ) 55 | state.widget.inject(mk('br')) 56 | state.widget.inject(chk) 57 | label = document.createElement('label') 58 | label.setAttribute("for", id) 59 | label.setAttribute("title", "Check this box to force all areas to be relative to their own projects (and not the compared projects). This may help to display foucs areas.") 60 | chk.setAttribute("title", "Check this box to force all areas to be relative to their own projects (and not the compared projects). This may help to display foucs areas.") 61 | label.style.paddingLeft = '5px' 62 | label.appendChild(document.createTextNode('Make all projects relative to themselves')) 63 | state.widget.inject(label) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Apache Kibble files 2 | api/yaml/kibble.yaml 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | # JetBrains IDE 144 | /.idea 145 | -------------------------------------------------------------------------------- /api/yaml/openapi/components/schemas/defaultWidgetArgs.yaml: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # defaultWidgetArgs # 3 | ######################################################################## 4 | properties: 5 | author: 6 | description: Turns on author view for code results, as opposed to the default 7 | committer view 8 | type: boolean 9 | collapse: 10 | description: for some widgets, this collapses sources based on a regex 11 | type: string 12 | email: 13 | description: filter sources based on an email (a person) 14 | type: string 15 | from: 16 | description: If specified, compile data from this timestamp onwards 17 | example: 1400000000 18 | type: integer 19 | interval: 20 | description: If fetching histograms, this specifies the interval to pack figures 21 | into. Can be day, week, month, quarter or year 22 | example: month 23 | type: string 24 | links: 25 | description: for relationship maps, this denotes the minimum link strength (no. 26 | of connections) that makes up a link. 27 | type: integer 28 | page: 29 | type: string 30 | quick: 31 | description: Turns on quick data for some endpoints, returning only sparse data 32 | (thus less traffic) 33 | example: false 34 | type: boolean 35 | search: 36 | description: for some widgets, this enables sub-filtering based on searches 37 | type: string 38 | source: 39 | description: If specified, only compile data on this specific sourceID 40 | example: abcdef12345678 41 | type: string 42 | sources: 43 | description: for some widget, this fetches all sources 44 | type: boolean 45 | span: 46 | description: For factor charts, denotes the number of months to base factors on 47 | from 48 | example: 2 49 | type: integer 50 | subfilter: 51 | description: Quickly defined view by sub-filtering the existing view and matching 52 | on sourceURLs 53 | type: string 54 | pathfilter: 55 | description: Quickly defined view by sub-filtering commits by paths affected. 56 | type: string 57 | to: 58 | description: If specified, only compile data up until here 59 | example: 1503483273 60 | type: integer 61 | types: 62 | description: If set, only return data from sources matching these types 63 | example: 64 | - jira 65 | - bugzilla 66 | type: array 67 | unique: 68 | description: Only compile data from unique commits, ignore duplicates 69 | type: boolean 70 | view: 71 | description: ID Of optional view to use 72 | example: abcdef12345678 73 | type: string 74 | distinguish: 75 | description: Enables distinguishing different types of data objects, subject to the individual API endpoint 76 | type: boolean 77 | example: false 78 | relative: 79 | description: Enables relative comparison mode for API endpoints that have this feature. 80 | type: boolean 81 | example: false 82 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_preferences.coffee: -------------------------------------------------------------------------------- 1 | preferences = (json, state) -> 2 | obj = document.createElement('form') 3 | 4 | items = 5 | screenname: 'Screen name' 6 | fullname : "Full name" 7 | email : "Email address" 8 | tag : "Organisation filter tag" 9 | token: "API token" 10 | desc = 11 | tag: "If set, only sources with this tag will be shown in your views." 12 | 13 | for item in ['screenname', 'fullname', 'email', 'tag', 'token'] 14 | div = mk('div') 15 | app(div, txt(items[item] + ": ")) 16 | inp = mk('input') 17 | set(inp, 'type', 'text') 18 | set(inp, 'name', item) 19 | inp.style.width = "200px" 20 | if item == 'token' 21 | set(inp, "readonly", "readonly") 22 | set(inp, "disabled", "disabled") 23 | inp.style.width = "700px" 24 | set(inp, 'value', if json[item] then json[item] else '') 25 | app(div, inp) 26 | if desc[item] 27 | i = mk('i') 28 | i.style.fontSize = "9pt" 29 | i.style.marginLeft = "20px" 30 | app(i, txt(desc[item])) 31 | app(div, i) 32 | app(obj, div) 33 | div = mk('div') 34 | app(div, txt("Organisation to view: ")) 35 | list = mk('select') 36 | set(list, 'name', 'organisation') 37 | for org in json.orgs 38 | opt = mk('option') 39 | opt.value = org 40 | opt.text = org 41 | if org == json.organisation 42 | opt.selected = 'selected' 43 | app(list, opt) 44 | app(div,list) 45 | app(obj, div) 46 | 47 | btn = mk('input') 48 | set(btn, 'type', 'button') 49 | set(btn, 'onclick', 'saveprefs(this.form)') 50 | set(btn, 'value', "Save preferences") 51 | app(obj, btn) 52 | 53 | #obj.innerHTML += JSON.stringify(json) 54 | state.widget.inject(obj, true) 55 | 56 | # Org admin? 57 | if json.admin 58 | aobj = mk('div') 59 | app(aobj, mk('br')) 60 | app(aobj, mk('br')) 61 | h1 = mk('h2') 62 | app(h1, txt("Organisation administration:")) 63 | app(aobj, h1) 64 | app(aobj, txt("If you are an organisation administrator, you may edit your organisation(s) by selecting the org you wish to edit below:")) 65 | for id, name of json.admin 66 | a = mk('a') 67 | set(a, 'href', '?page=orgadmin&org=' + id) 68 | h3 = mk('h4') 69 | app(h3, txt("- " + name)) 70 | app(a, h3) 71 | app(aobj, a) 72 | state.widget.inject(aobj) 73 | 74 | saveprefs = (form) -> 75 | js = { 76 | action: 'save' 77 | } 78 | for i in [0..form.length-1] 79 | k = form[i].name 80 | v = form[i].value 81 | if k in ['screenname', 'fullname', 'email', 'tag', 'organisation'] 82 | js[k] = v 83 | postJSON("preferences", js, null, (a) -> alert("Preferences saved!") ) 84 | -------------------------------------------------------------------------------- /setup/makeaccount.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Licensed to the Apache Software Foundation (ASF) under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # The ASF licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import sys, os, os.path 18 | import elasticsearch 19 | import argparse 20 | import yaml 21 | import bcrypt 22 | 23 | import sys 24 | sys.path.append('../') 25 | 26 | import api.plugins.database 27 | 28 | arg_parser = argparse.ArgumentParser() 29 | arg_parser.add_argument("-u", "--username", required=True, help="Username (email) of accoun to create") 30 | arg_parser.add_argument("-p", "--password", required=True, help="Password to set for account") 31 | arg_parser.add_argument("-n", "--name", help="Real name (displayname) of account (optional)") 32 | arg_parser.add_argument("-A", "--admin", action="store_true", help="Make account global admin") 33 | arg_parser.add_argument("-a", "--orgadmin", action="store_true", help="Make account owner of orgs invited to") 34 | arg_parser.add_argument("-o", "--org", help="Invite to this organisation (id)") 35 | 36 | args = arg_parser.parse_args() 37 | 38 | # Load Kibble master configuration 39 | config = yaml.load(open("../api/yaml/kibble.yaml"), Loader=yaml.Loader) 40 | 41 | # use es 7 mapping if 42 | DB = api.plugins.database.KibbleDatabase(config) 43 | 44 | username = args.username 45 | password = args.password 46 | name = args.name if args.name else args.username 47 | admin = True if args.admin else False 48 | adminorg = True if args.orgadmin else False 49 | orgs = [args.org] if args.org else [] 50 | aorgs = orgs if adminorg else [] 51 | 52 | salt = bcrypt.gensalt() 53 | pwd = bcrypt.hashpw(password.encode('utf-8'), salt) #.decode('ascii') 54 | 55 | doc = { 56 | 'email': username, # Username (email) 57 | 'password': pwd, # Hashed password 58 | 'displayName': username, # Display Name 59 | 'organisations': orgs, # Orgs user belongs to (default is none) 60 | 'ownerships': aorgs, # Orgs user owns (default is none) 61 | 'defaultOrganisation': None, # Default org for user 62 | 'verified': True, # Account verified via email? 63 | 'userlevel': "admin" if admin else "user" # User level (user/admin) 64 | } 65 | 66 | # doc_type is adpated for es > 6 67 | res = DB.ES.index(index=DB.dbname, doc_type='useraccount', id = username, body = doc) # 68 | 69 | print("Account '%s' %s in index %s!" %( username, res['result'], DB.dbname) ) 70 | 71 | -------------------------------------------------------------------------------- /ui/js/coffee/colors.coffee: -------------------------------------------------------------------------------- 1 | 2 | hsl2rgb = (h, s, l) -> 3 | 4 | h = h % 1; 5 | s = 1 if s > 1 6 | l = 1 if l > 1 7 | if l <= 0.5 8 | v = (l * (1 + s)) 9 | else 10 | v = (l + s - l * s); 11 | if v == 0 12 | return { 13 | r: 0, 14 | g: 0, 15 | b: 0 16 | } 17 | 18 | min = 2 * l - v; 19 | sv = (v - min) / v; 20 | sh = (6 * h) % 6; 21 | switcher = Math.floor(sh); 22 | fract = sh - switcher; 23 | vsf = v * sv * fract; 24 | 25 | switch switcher 26 | when 0 27 | return { 28 | r: v, 29 | g: min + vsf, 30 | b: min 31 | }; 32 | when 1 33 | return { 34 | r: v - vsf, 35 | g: v, 36 | b: min 37 | }; 38 | when 2 39 | return { 40 | r: min, 41 | g: v, 42 | b: min + vsf 43 | }; 44 | when 3 45 | return { 46 | r: min, 47 | g: v - vsf, 48 | b: v 49 | }; 50 | when 4 51 | return { 52 | r: min + vsf, 53 | g: min, 54 | b: v 55 | }; 56 | when 5 57 | return { 58 | r: v, 59 | g: min, 60 | b: v - vsf 61 | }; 62 | 63 | return { 64 | r: 0, 65 | g: 0, 66 | b: 0 67 | }; 68 | 69 | 70 | genColors = (numColors, saturation, lightness, hex) -> 71 | cls = [] 72 | baseHue = 1.02; 73 | if numColors <= 2 74 | baseHue = 0.65 75 | for i in [1..numColors] 76 | c = hsl2rgb(baseHue, saturation, lightness) 77 | while (c.r > 0.8 and c.g > 0.8 and c.b > 0.8) 78 | baseHue -= 0.37 79 | if baseHue < 0 80 | baseHue += 1 81 | c = hsl2rgb(baseHue, saturation, lightness) 82 | if (hex) 83 | #h = ( Math.round(c.r*255*255*255) + Math.round(c.g * 255*255) + Math.round(c.b*255) ).toString(16) 84 | h = "#" + ("00" + (~ ~(c.r * 255)).toString(16)).slice(-2) + ("00" + (~ ~(c.g * 255)).toString(16)).slice(-2) + ("00" + (~ ~(c.b * 255)).toString(16)).slice(-2); 85 | cls.push(h); 86 | else 87 | cls.push({ 88 | r: parseInt(c.r * 255), 89 | g: parseInt(c.g * 255), 90 | b: parseInt(c.b * 255) 91 | }) 92 | baseHue -= 0.37 93 | if (baseHue < 0) 94 | baseHue += 1 95 | 96 | return cls 97 | 98 | 99 | quickColors = (num) -> 100 | colors = [] 101 | ph = 0 102 | for x in [1..num] 103 | r = Math.random() 104 | g = Math.random() 105 | b = Math.random() 106 | 107 | pastel = 0.7 108 | r = ((pastel+r)/2) 109 | g = ((pastel+g)/2) 110 | b = ((pastel+b)/2) 111 | 112 | c = "#" + ("00" + (~ ~(r * 205)).toString(16)).slice(-2) + ("00" + (~ ~(g * 205)).toString(16)).slice(-2) + ("00" + (~ ~(b * 205)).toString(16)).slice(-2); 113 | colors.push(c) 114 | return colors 115 | -------------------------------------------------------------------------------- /api/pages/verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/verify/{email}/{vcode} 19 | ######################################################################## 20 | # get: 21 | # summary: Verify an account 22 | # parameters: 23 | # - name: email 24 | # in: path 25 | # description: Email address of account 26 | # required: true 27 | # schema: 28 | # type: string 29 | # - name: vcode 30 | # in: path 31 | # description: Verification code 32 | # required: true 33 | # schema: 34 | # type: string 35 | # responses: 36 | # '200': 37 | # description: 200 Response 38 | # content: 39 | # application/json: 40 | # schema: 41 | # $ref: '#/components/schemas/ActionCompleted' 42 | # default: 43 | # description: unexpected error 44 | # content: 45 | # application/json: 46 | # schema: 47 | # $ref: '#/components/schemas/Error' 48 | # 49 | ######################################################################## 50 | 51 | 52 | 53 | 54 | 55 | """ 56 | This is the user account verifier for Kibble. 57 | """ 58 | 59 | 60 | def run(API, environ, indata, session): 61 | 62 | # Get vocde, make sure it's 40 chars 63 | vcode = indata.get('vcode') 64 | if len(vcode) != 40: 65 | raise API.exception(400, "Invalid verification code!") 66 | 67 | # Find the account with this vcode 68 | email = indata.get('email') 69 | if len(email) < 7: 70 | raise API.exception(400, "Invalid email address presented.") 71 | 72 | if session.DB.ES.exists(index=session.DB.dbname, doc_type='useraccount', id = email): 73 | doc = session.DB.ES.get(index=session.DB.dbname, doc_type='useraccount', id = email) 74 | # Do the codes match?? 75 | if doc['_source']['vcode'] == vcode: 76 | doc['_source']['verified'] = True 77 | # Save account as verified 78 | session.DB.ES.index(index=session.DB.dbname, doc_type='useraccount', id = email, body = doc['_source']) 79 | yield("Your account has been verified, you can now log in!") 80 | else: 81 | raise API.exception(404, "Invalid verification code presented!") 82 | else: 83 | raise API.exception(404, "Invalid verification code presented!") # Don't give away if such a user exists, pssst 84 | -------------------------------------------------------------------------------- /api/yaml/sourcetypes.yaml: -------------------------------------------------------------------------------- 1 | git: 2 | title: "Plain git repository" 3 | description: This is a plain git repository with no issues/PRs attached. For GitHub repositories, use the GitHub source type. 4 | regex: (?:https?|git)://.*/.+\.git 5 | example: "git://example.org/repos/foo.git" 6 | optauth: 7 | - username 8 | - password 9 | 10 | github: 11 | title: "GitHub repository (plus issues/PRs)" 12 | description: "This is GitHub repositories with issues and pull requests. For non-GitHub repos, please use the plain 'git' source type" 13 | regex: "https://github.com/.+/.+" 14 | example: "https://github.com/apache/kibble.git" 15 | authrequired: true 16 | optauth: 17 | - username 18 | - password 19 | 20 | jira: 21 | title: "JIRA Project" 22 | description: "This is a JIRA project. It requires a user account with read credentials to operate optimally." 23 | regex: "https://.+/browse/[A-Z0-9-]+" 24 | example: "https://issues.apache.org/jira/browse/ZEST" 25 | authrequired: true 26 | optauth: 27 | - username 28 | - password 29 | 30 | bugzilla: 31 | title: "BugZilla Project" 32 | regex: https?://.+/jsonrpc\.cgi?\s+.+ 33 | example: "https://bz.apache.org/bugzilla/jsonrpc.cgi Apache2" 34 | optauth: 35 | - username 36 | - password 37 | 38 | ponymail: 39 | title: "Pony Mail Archive" 40 | description: "A Pony Mail archive - add lists one by one. Requires a session cookie to operate optimally." 41 | regex: "https?://.+/list.html?.+@.+" 42 | example: "https://lists.apache.org/list.html?dev@spamassassin.apache.org" 43 | authrequired: true 44 | optauth: 45 | - cookie 46 | 47 | pipermail: 48 | title: "Pipermail archive" 49 | regex: "https?://.+/(archives|pipermail)/.+/" 50 | example: "https://www.redhat.com/archives/libvir-list/" 51 | 52 | gerrit: 53 | title: "Gerrit Code Review Project" 54 | regex: "https?://.+/r/.+" 55 | example: https://review.rdoproject.org/r/puppet/puppet-designate 56 | 57 | jenkins: 58 | title: Jenkins CI Server 59 | description: A Jenkins CI Server - Grabs all jobs in one go. 60 | regex: "https?://.+/" 61 | example: https://builds.apache.org/ 62 | optauth: 63 | - username 64 | - password 65 | 66 | buildbot: 67 | title: Buildbot CI Server 68 | description: A Buildbot CI Server - Grabs all jobs in one go. 69 | regex: "https?://.+/" 70 | example: https://ci.apache.org/ 71 | optauth: 72 | - username 73 | - password 74 | 75 | travis: 76 | title: Travis CI Server 77 | description: The Travis CI Service - Grabs all jobs in one go. Specify .com or .org 78 | regex: "https?://travis-ci[.](com|org)/?" 79 | example: https://travis-ci.org/ 80 | authrequired: true 81 | optauth: 82 | - token 83 | 84 | 85 | twitter: 86 | title: "Twitter Handle" 87 | regex: (@.+) 88 | example: "@ApacheKibble" 89 | authrequired: true 90 | optauth: 91 | - token 92 | - token_secret 93 | - consumer_key 94 | - consumer_secret 95 | 96 | discourse: 97 | title: Discourse 98 | description: A Discourse Forum System. 99 | regex: "https?://.+/" 100 | example: https://discourse.example.com/ 101 | optauth: 102 | - username 103 | - password -------------------------------------------------------------------------------- /ui/js/coffee/widget_admin.coffee: -------------------------------------------------------------------------------- 1 | orgadmin = (json, state) -> 2 | 3 | if globArgs.org and json.admin[globArgs.org] 4 | pdiv = document.createElement('div') 5 | id = globArgs.org 6 | title = json.admin[id] 7 | h2 = mk('h2') 8 | app(h2, txt("Editing: " + title)) 9 | app(pdiv, h2) 10 | 11 | obj = mk('form') 12 | h4 = mk('h4') 13 | app(h4, txt("Invite a new user to this org:")) 14 | app(obj, h4) 15 | 16 | div = mk('div') 17 | app(div, txt("Username (email): ")) 18 | inp = mk('input') 19 | set(inp, 'type', 'text') 20 | set(inp, 'name', 'who') 21 | inp.style.width = "200px" 22 | app(div, inp) 23 | app(obj, div) 24 | 25 | div = mk('div') 26 | app(div, txt("Make administrator: ")) 27 | inp = mk('input') 28 | set(inp, 'type', 'checkbox') 29 | set(inp, 'name', 'admin') 30 | set(inp, 'value', 'true') 31 | app(div, inp) 32 | app(obj, div) 33 | 34 | btn = mk('input') 35 | set(btn, 'type', 'button') 36 | set(btn, 'onclick', 'addorguser(this.form)') 37 | set(btn, 'value', "Add user") 38 | app(obj, btn) 39 | 40 | app(pdiv, obj) 41 | 42 | 43 | obj = mk('form') 44 | h4 = mk('h4') 45 | app(h4, txt("Remove a user from the org:")) 46 | app(obj, h4) 47 | 48 | div = mk('div') 49 | app(div, txt("Username (email): ")) 50 | inp = mk('input') 51 | set(inp, 'type', 'text') 52 | set(inp, 'name', 'who') 53 | inp.style.width = "200px" 54 | app(div, inp) 55 | app(obj, div) 56 | 57 | div = mk('div') 58 | app(div, txt("Just remove admin privs (if any): ")) 59 | inp = mk('input') 60 | set(inp, 'type', 'checkbox') 61 | set(inp, 'name', 'admin') 62 | set(inp, 'value', 'true') 63 | app(div, inp) 64 | app(obj, div) 65 | 66 | btn = mk('input') 67 | set(btn, 'type', 'button') 68 | set(btn, 'onclick', 'remorguser(this.form)') 69 | set(btn, 'value', "Remove user") 70 | app(obj, btn) 71 | 72 | app(pdiv, obj) 73 | 74 | state.widget.inject(pdiv, true) 75 | else 76 | state.widget.inject(txt("You are not an admin of this organisation!")) 77 | 78 | 79 | 80 | addorguser = (form) -> 81 | js = { 82 | action: 'add', 83 | org: globArgs.org 84 | } 85 | for i in [0..form.length-1] 86 | k = form[i].name 87 | v = form[i].value 88 | if k == 'who' 89 | form[i].value = "" 90 | if k == 'admin' 91 | v = if form[i].checked then 'true' else 'false' 92 | if k in ['who', 'admin'] 93 | js[k] = v 94 | 95 | postJSON("admin-org", js, null, (a) -> alert("User added!") ) 96 | 97 | remorguser = (form) -> 98 | js = { 99 | action: 'remove', 100 | org: globArgs.org 101 | } 102 | for i in [0..form.length-1] 103 | k = form[i].name 104 | v = form[i].value 105 | if k == 'who' 106 | form[i].value = "" 107 | if k == 'admin' 108 | v = if form[i].checked then 'true' else 'false' 109 | if k in ['who', 'admin'] 110 | js[k] = v 111 | 112 | postJSON("admin-org", js, null, (a) -> alert("User removed!") ) 113 | -------------------------------------------------------------------------------- /ui/js/coffee/charts_punchcard.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | punchcard_color = (p, MAX) -> 19 | v = (p/MAX) 20 | hue=((1-v)*120).toString(10) 21 | return "hsl(#{hue},80%,40%)" 22 | 23 | 24 | pval = (d,m) -> 25 | v = Math.max(d, m/3) 26 | if v > m/3 27 | v = (v+(m/3)) / m 28 | else 29 | v = 0.33 30 | 31 | charts_punchcard = (obj, data, options) -> 32 | div = document.getElementById('tooltip_punchcard') 33 | if not div 34 | div = d3.select("body").append("div").attr("class", "punchcard_tooltip").attr('id', 'tooltip_punchcard').style("opacity", 0) 35 | else 36 | div = d3.select(div) 37 | data = data.timeseries 38 | days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 39 | 40 | c = [] 41 | 42 | chart = d3.select(obj).append("svg").attr("width", '100%').attr("height", '100%') 43 | 44 | 45 | MAX = 0 46 | for k, v of data 47 | m = k.split(/ - /) 48 | a = m[0] 49 | b = m[1] 50 | ypos = 0 51 | if b == '24' 52 | b = 0 53 | xpos = 0.1 + (parseInt(b) ) * (0.036) 54 | for n,d of days 55 | if d == a 56 | ypos = 0.04 + (n * 0.10) 57 | c.push({x: xpos, y: ypos, r: v, h: "#{a}, #{b}:00 -> #{(parseInt(b)+1) % 24}:00 UTC
"}) 58 | MAX = Math.max(MAX, v) 59 | circles = chart.selectAll('svg').data(c).enter().append("circle"); 60 | labels = chart.selectAll('svg').data(days).enter().append('text') 61 | slots = chart.selectAll('svg').data([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]).enter().append('text') 62 | 63 | 64 | redraw = () -> 65 | xy = obj.getBoundingClientRect() 66 | xy.height = xy.width * 0.5 67 | chart.attr('width', xy.width + 'px') 68 | chart.attr('height', xy.height + 'px') 69 | maxr = Math.sqrt(xy.width**2 + xy.height**2) / 80 70 | cw = (0.03*xy.width) 71 | circles.attr("cx", (d) => (d.x*xy.width) + cw/2).attr("cy", (d) => 50 + d.y*xy.height ).attr("r", (d) => pval(d.r, MAX) * maxr).style("fill", (d) => punchcard_color(d.r, MAX)). 72 | on("mouseover", (d) -> 73 | div.transition() 74 | .duration(200) 75 | .style("opacity", .9) 76 | div .html(d.h + d.r.pretty() + " commits") 77 | .style("left", (d3.event.pageX) + "px") 78 | .style("top", (d3.event.pageY - 28) + "px"); 79 | ).on("mouseout", (d) -> 80 | div.transition() 81 | .duration(200) 82 | .style("opacity", 0) 83 | ) 84 | labels.attr('x', 20).attr('y', (d) => (55 + (0.04 + days.indexOf(d) * 0.10) * xy.height)).attr('font-size', maxr*1.75).text((d) => d) 85 | slots.attr('x', (d) => (0.1 + d * 0.036) * xy.width + cw/2).attr('y',38).attr('text-anchor', 'middle').attr('width', cw).attr('font-size', maxr*1.5).text((d) => d) 86 | chart.node().addEventListener("resize", redraw) 87 | window.addEventListener("resize", redraw) 88 | redraw(); 89 | 90 | return [chart, {punchcard: true}] 91 | 92 | -------------------------------------------------------------------------------- /docs/source/usecases.rst: -------------------------------------------------------------------------------- 1 | Use Cases 2 | ======================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | ********************** 9 | Add an Organisation 10 | ********************** 11 | This use case describes the process of adding an organisation 12 | 13 | Actors: 14 | User 15 | 16 | Precondition: 17 | User is logged in 18 | 19 | Flow of Events: 20 | 1. The use case starts when the user is on the Organisation tab. 21 | 2. The system loads any previously created organisations and the form to create a new organisation 22 | 3. The user can enter an organisation name, description, and ID. 23 | 4. The system will verify the information. 24 | 5. The system will add the new organisation. 25 | 6. The system will then display the new organisation along with any existing organisations. 26 | 27 | Exception Scenario: 28 | The user does not enter an organisation name or description. 29 | 30 | Post Conditions: 31 | The user creates the organisation or leaves the page. 32 | 33 | 34 | ********************** 35 | Add a View 36 | ********************** 37 | This use case describes the process of adding a view to an organisation 38 | 39 | Actors: 40 | User 41 | 42 | Precondition: 43 | User is logged in and has an organisation created 44 | 45 | Flow of Events: 46 | 1. The use case starts when the user is on the Views tab. 47 | 2. The user will click on the "Create a new view" button. 48 | 3. The system loads the form to create a new view. 49 | 4. The user will information needed to create the view. 50 | 5. The system will verify the information. 51 | 6. The system will add the new view. 52 | 7. The system will then display the new view along with any existing views. 53 | 8. The user with then be able to edit or delete the view. 54 | 55 | Exception Scenario: 56 | The user does not enter a view name. 57 | 58 | Post Conditions: 59 | The user creates the source or leaves the page. 60 | 61 | 62 | ********************** 63 | Add a Source 64 | ********************** 65 | This use case describes the process of adding a source to an organisation 66 | 67 | Actors: 68 | User 69 | 70 | Precondition: 71 | User is logged in and has an organisation created 72 | 73 | Flow of Events: 74 | 1. The use case starts when the user is on the Sources tab. 75 | 2. The system loads any existing sources and the form to create a new source. 76 | 3. The user will select a source type from the list provided. 77 | 4. The user will enter a source URL/ID and a username and password if needed. 78 | 5. The system will verify the information. 79 | 6. The system will add the new source. 80 | 7. The system will then display the new source along with any existing sources. 81 | 8. The user with then have to run the kibble scanner to process the new source. 82 | 83 | Exception Scenario: 84 | The user does not enter a source URL/ID. 85 | 86 | Post Conditions: 87 | The user creates the source or leaves the page. 88 | 89 | 90 | ********************** 91 | Add a User 92 | ********************** 93 | This use case describes the process of adding a user to an organisation 94 | 95 | Actors: 96 | User 97 | 98 | Precondition: 99 | User is logged in and has an organisation created 100 | 101 | Flow of Events: 102 | 1. The use case starts when the user is on the Users tab. 103 | 2. The system loads the form to invite a new member and the current membership of the organisation. 104 | 3. The user will enter the email address of a user. 105 | 4. The system will verify the information. 106 | 5. The system will add the user to the organisation's membership. 107 | 108 | Exception Scenario: 109 | The user enters a user that does not exist. 110 | 111 | Post Conditions: 112 | The user invites a member or leaves the page. 113 | 114 | 115 | -------------------------------------------------------------------------------- /ui/contributors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard - Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 40 |
41 | 65 |
66 |
67 |
68 |
69 | Loading, please wait... 70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /ui/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard - Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 35 | 41 |
42 | 68 |
69 |
70 |
71 |
72 | Loading, please wait... 73 |
74 |
75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/source/managing.rst: -------------------------------------------------------------------------------- 1 | Managing Apache Kibble 2 | ====================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | 9 | ************************ 10 | Creating an Organisation 11 | ************************ 12 | 13 | The first thing you will need to set up, in order to use Kibble, is an 14 | organisation that will contain the projects you wish to survey. You can 15 | have multiple organisations in Kibble, and all organisations will be 16 | scanned, but the UI will only display statistics for the current 17 | (default) organisation you are using. You may switch between 18 | organisations at your leisure in the UI. 19 | 20 | To create your first organisation: 21 | 22 | 1. Go to the "Organisation" tab in the top menu 23 | 2. Locate the Create a new organisation` field set 24 | 3. Enter the details required for the new organsation 25 | 26 | This will set up a new organisation and set it as your default (current) 27 | one. 28 | 29 | Once an organisation has been created, you can then add resources and 30 | users to it. 31 | 32 | .. _configdatasources: 33 | 34 | ************************ 35 | Configuring Data Sources 36 | ************************ 37 | 38 | After you have created an organisation, you can add sources to it. 39 | A source is a destination to scan; it can be a git repository, a 40 | JIRA instance, a mailing list and so on. To start adding sources, click 41 | on the `Sources` tab in the left hand menu on the `Organisation` page. 42 | 43 | With all resource types, you can speed up things by adding multiple 44 | sources in one go by simply adding one source per line in the source 45 | text field. 46 | 47 | The currently supported resource types are: 48 | 49 | GitHub 50 | This resource consists of GitHub repositories as well as issues/PRs 51 | that are contained within. Currently, you will need to add the full 52 | URL to the repo, including the `.git` part of it, such as: 53 | ``https://github.com/apache/clerezza.git``. 54 | **NOTE**: If you intend to use more than 60 API calls per hour, which 55 | you probably do, you will need to add the credentials of a GitHub 56 | user to the source, in order to get a higher rate limit of 6,000 API 57 | calls per hour. You may use any anonymous account for this. 58 | 59 | Git 60 | This is a plain git repository (such as those served by the standard 61 | git daemon), and only scans repositories, not PRs/Issues. If basic 62 | auth is required, fill our the user/pass credentials, otherwise leave 63 | it blank. 64 | 65 | PiperMail 66 | This is the standard MailMan 2.x list service. The URL should be the 67 | full path to the directory that shows the various months 68 | 69 | Pony Mail 70 | This is a Pony Mail list. It should be in the form of 71 | `list.html?foo@bar.baz` and you *should* include a session cookie in 72 | order to bypass email address anonymization where applicable. If the 73 | Pony Mail instance does not apply anonymization, you may leave the 74 | cookie blank. 75 | 76 | Gerrit 77 | This is a gerrit code review resource, and will scan for tickets, 78 | authors etc. 79 | 80 | BugZilla 81 | This is a BugZilla ticket instance. You should add one source for 82 | each BugZilla project you wish to scan. It should point to the 83 | JSONRPC CGI file followed by the project you wish to scan. 84 | If you wish to just add everything as one source, 85 | you can do so by pointing it at ``jsonrpc.cgi *`` which will scan 86 | everything in the BugZilla database. If you want to be able to 87 | look at individual projects, it's recommended that you scan them 88 | individually. 89 | 90 | JIRA 91 | This is a JIRA project. Most JIRA instances will require the login 92 | credentials of an anonymous account in order to perform API calls. 93 | 94 | Twitter 95 | This is a Twitter account. Currently not much done there. WIP. 96 | 97 | Jenkins CI 98 | This is a Jenkins CI instance. One URL is required, and all sources 99 | will be scanned. 100 | 101 | Buildbot CI 102 | This is a Buildbot instance. One URL is required, and all sources 103 | will be scanned in one go. 104 | 105 | Once you have added the resource URLs you wish to analyse, you 106 | can obtain data by following the instructions in the chapter 107 | :ref:`runscan`. 108 | 109 | **************** 110 | Adding New Users 111 | **************** 112 | 113 | MORE TODO 114 | -------------------------------------------------------------------------------- /ui/engagement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard - Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 40 |
41 | 66 |
67 |
68 |
69 |
70 | Loading, please wait... 71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ui/organisations.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard - Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 36 | 42 |
43 | 68 |
69 |
70 |
71 |
72 | Loading, please wait... 73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ui/relationships.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Relationships - Apache Kibble 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 40 |
41 | 66 |
67 |
68 |
69 |
70 | Loading, please wait... 71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_treemap.coffee: -------------------------------------------------------------------------------- 1 | treemap = (json, state) -> 2 | div = document.createElement('div') 3 | cats = new Array() 4 | dates = new Array() 5 | catdata = {} 6 | 7 | filled = { areaStyle: {type: 'default' } } 8 | if json.widgetType 9 | if json.widgetType.chartType 10 | type = json.widgetType.chartType 11 | if json.widgetType.stack 12 | stack = json.widgetType.stack 13 | if json.widgetType.nofill 14 | filled = null 15 | #if state.widget.args.representation 16 | #type = state.widget.args.representation 17 | if not json.widget.title or json.widget.title.length == 0 18 | json.widget.title = 'Languages' 19 | 20 | if not state.widget.div.style.height 21 | div.style.minHeight = "900px" 22 | else 23 | div.style.minHeight = "100%" 24 | if state.widget.fullscreen 25 | div.style.minHeight = (window.innerHeight - 100) + "px" 26 | 27 | state.widget.inject(div, true) 28 | 29 | 30 | 31 | range = "" 32 | rect = div.getBoundingClientRect() 33 | theme.color = genColors(json.treemap.length+1, 0.6, 0.5, true) 34 | colors = genColors(json.treemap.length+1, 0.6, 0.5, true) 35 | theme.textStyle.fontSize = Math.max(12, window.innerHeight/100) 36 | echartLine = echarts.init(div, theme); 37 | 38 | ld = [] 39 | for lang, i in json.treemap 40 | ld.push(lang) 41 | for project in lang 42 | project.color = colors[i] 43 | project.itemStyle = { 44 | normal: { 45 | color: colors[i] 46 | } 47 | } 48 | 49 | option = { 50 | 51 | title: { 52 | text: json.widget.title, 53 | left: 'center' 54 | }, 55 | legend: [{ 56 | x: 'center', 57 | y: 'top', 58 | #selectedMode: 'single', 59 | data: ld 60 | }], 61 | 62 | tooltip: { 63 | show: true, 64 | feature: { 65 | saveAsImage: { 66 | show: true, 67 | title: "Save Image" 68 | } 69 | }, 70 | formatter: (info) -> 71 | value = info.value; 72 | treePathInfo = info.treePathInfo; 73 | treePath = []; 74 | 75 | for i in [1...treePathInfo.length] 76 | treePath.push(treePathInfo[i].name) 77 | 78 | 79 | return [ 80 | '
' + treePath.join('/') + '
', 81 | 'Lines of Code: ' + value.pretty(), 82 | ].join(''); 83 | 84 | }, 85 | 86 | series: [ 87 | { 88 | name:json.widget.title, 89 | type:'treemap', 90 | visibleMin: 1000, 91 | label: { 92 | show: true, 93 | formatter: '{b}' 94 | }, 95 | itemStyle: { 96 | normal: { 97 | borderColor: '#fff' 98 | } 99 | }, 100 | levels: [{ 101 | itemStyle: { 102 | normal: { 103 | borderColor: '#555', 104 | borderWidth: 4, 105 | gapWidth: 4 106 | } 107 | } 108 | }, 109 | { 110 | colorSaturation: [0.3, 0.6], 111 | itemStyle: { 112 | normal: { 113 | borderColorSaturation: 0.7, 114 | gapWidth: 2, 115 | borderWidth: 2 116 | } 117 | } 118 | } 119 | ], 120 | data: json.treemap 121 | } 122 | ] 123 | } 124 | echartLine.setOption(option = option); 125 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_top5.coffee: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | make5 = (obj, json, pos) -> 17 | what = json.topN.denoter 18 | for item, i in json.topN.items[pos...pos+5] 19 | if i == 5 20 | break 21 | idiv = new HTML('div', { class: "media event"} ) 22 | left = new HTML('a', { class: "pull-left"}) 23 | if item.gravatar 24 | left.inject(new HTML('img', { class: "img-circle img-reponsive", src: "https://secure.gravatar.com/avatar/#{item.gravatar}.png?d=identicon" ,style: { width: "32px", height: "32px"}})) 25 | else if json.topN.icon 26 | left.inject(new HTML('i', { class: "fa fa-#{json.topN.icon}", style: { fontSize: "28px"}})) 27 | right = new HTML('div', { class: "media event"}) 28 | rightInner = new HTML('div', { class: "media-body"}) 29 | right.inject(rightInner) 30 | if item.email 31 | title = new HTML('a', { class: "title", href:"contributors.html?page=biography&email=#{item.email}"}, txt(item.name)) 32 | rightInner.inject(title) 33 | rightInner.inject(" - ") 34 | filter = new HTML('a', { class: "title", href:"javascript: void(filterPerson('#{item.email}'));"}, "[filter]") 35 | rightInner.inject(filter) 36 | else if item.url 37 | if item.title 38 | item.tooltip = item.title 39 | if item.title.length > 40 40 | item.title = item.title.toString().substring(0,40) + "..." 41 | item.name += ": " + item.title 42 | # Sometimes, scanners add a spurious extra slash. nix it. 43 | item.url = item.url.replace(/([^:])(\/\/+)/g, '$1/') 44 | title = new HTML('a', { title: item.tooltip, class: "title", href:item.url}, txt(item.name)) 45 | rightInner.inject(title) 46 | else 47 | title = new HTML('a', { class: "title"}, txt(item.name)) 48 | rightInner.inject(title) 49 | fodder = new HTML('p', {}) 50 | fodder.inject(new HTML('b', {}, item.count.pretty())) 51 | fodder.inject(txt(" #{what} during this period")) 52 | if item.subcount 53 | fodder.inject(new HTML('br')) 54 | t = [] 55 | for k,v of item.subcount 56 | t.push(v.pretty() + " " + k) 57 | fodder.inject(new HTML('small', {}, (t.join(", ") + "."))) 58 | rightInner.inject(fodder) 59 | idiv.inject(left) 60 | idiv.inject(right) 61 | obj.inject(idiv) 62 | 63 | top5 = (json, state) -> 64 | items = [] 65 | if json.topN 66 | id = parseInt(Math.random()*99999999).toString(16) 67 | obj = new HTML('div', { id: id}) 68 | make5(obj, json, 0) 69 | state.widget.inject(obj, true) 70 | pos = 5 71 | while pos < json.topN.items.length 72 | nid = id + "_show_" + pos 73 | 74 | obj.inject(new HTML('a', { style: { cursor: 'pointer'}, onclick: "this.style.display = 'none'; get('#{nid}').style.display = 'block';"}, "Show more...")) 75 | obj = new HTML('div', { id: nid, style: { display: 'none'}}) 76 | make5(obj, json, pos) 77 | state.widget.inject(obj) 78 | pos += 5 79 | 80 | 81 | 82 | showMore = (id) -> 83 | obj = document.getElementById(id) 84 | if obj 85 | obj.style.display = "block" 86 | 87 | 88 | filterPerson = (email) -> 89 | if email == "" 90 | email = null 91 | updateWidgets('donut', null, { email: email }) 92 | updateWidgets('line', null, { email: email }) 93 | updateWidgets('contacts', null, { email: email }) 94 | updateWidgets('top5', null, { email: email }) 95 | updateWidgets('trends', null, { email: email }) 96 | updateWidgets('relationship', null, { email: email }) 97 | updateWidgets('viewpicker', null, { email: email }) 98 | globArgs.email = email 99 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_bio.coffee: -------------------------------------------------------------------------------- 1 | bio = (json, state) -> 2 | obj = document.createElement('div') 3 | if json.found 4 | # First commit 5 | firstcommit = "Never" 6 | if json.bio.firstCommit 7 | firstcommit = new Date(json.bio.firstCommit*1000).toDateString() 8 | # First authorship 9 | firstauthor = "Never" 10 | if json.bio.firstAuthor 11 | firstauthor = new Date(json.bio.firstAuthor*1000).toDateString() 12 | # First email 13 | firstemail = "Never" 14 | if json.bio.firstEmail 15 | firstemail = new Date(json.bio.firstEmail*1000).toDateString() 16 | 17 | bioOuter = new HTML('div', { class: 'media-event'} ) 18 | bioOuter.inject(new HTML('a', { class: 'pull-left bio-image'}, 19 | new HTML('img', { style: "width: 128px; height: 128px;", src: 'https://secure.gravatar.com/avatar/' + json.bio.gravatar + '.png?d=identicon&size=128'}) 20 | )) 21 | bioInner = new HTML('div', class: 'media-body bio-profile') 22 | bioInner.inject(new HTML('h2', {}, json.bio.name)) 23 | bioInner.inject(new HTML('h3', {}, json.bio.email)) 24 | bioInner.inject(new HTML('hr', {})) 25 | bioInner.inject(new HTML('div', {class: 'bio-fact'}, 26 | [ 27 | new HTML('strong', {}, 'First code commit'), 28 | new HTML('br'), 29 | new HTML('span', {}, firstcommit) 30 | ])) 31 | bioInner.inject(new HTML('div', {class: 'bio-fact'}, 32 | [ 33 | new HTML('strong', {}, 'First code authorship'), 34 | new HTML('br'), 35 | new HTML('span', {}, firstauthor) 36 | ])) 37 | bioInner.inject(new HTML('div', {class: 'bio-fact'}, 38 | [ 39 | new HTML('strong', {}, 'First email'), 40 | new HTML('br'), 41 | new HTML('span', {}, firstemail) 42 | ])) 43 | bioInner.inject(new HTML('div', {class: 'bio-fact'}, 44 | [ 45 | new HTML('strong', {}, 'Commits'), 46 | new HTML('br'), 47 | new HTML('span', {}, json.bio.commits.pretty()) 48 | ])) 49 | bioInner.inject(new HTML('div', {class: 'bio-fact'}, 50 | [ 51 | new HTML('strong', {}, 'Emails'), 52 | new HTML('br'), 53 | new HTML('span', {}, json.bio.emails.pretty()) 54 | ])) 55 | bioOuter.inject(bioInner) 56 | obj.appendChild(bioOuter) 57 | namecard = mk('h2') 58 | groups = [] 59 | if json.bio.tags 60 | for tag in json.bio.tags 61 | if tag != '_untagged' 62 | groups.push(tag) 63 | if groups.length > 0 64 | namecard.appendChild(mk('br')) 65 | namecard.appendChild(txt("Part of: " + groups.join(", "))) 66 | a = mk('a') 67 | set(a, 'href', 'javascript:void(affiliate("' + json.bio.id + '"));') 68 | app(a, txt("Set a tag")) 69 | 70 | egroups = [] 71 | if json.bio.alts and json.bio.alts.length 72 | for tag in json.bio.alts 73 | egroups.push(tag) 74 | if egroups.length > 0 75 | namecard.appendChild(mk('br')) 76 | namecard.appendChild(txt("Also known as: " + egroups.join(", "))) 77 | a2 = mk('a') 78 | a2.style.marginLeft = "8px" 79 | set(a2, 'href', 'javascript:void(altemail("' + json.bio.id + '"));') 80 | app(a2, txt("Add alt email")) 81 | 82 | sp = mk('span') 83 | set(sp, 'id', 'tags_' + json.bio.id) 84 | app(obj, namecard) 85 | app(obj, a) 86 | app(obj, a2) 87 | app(obj, sp) 88 | 89 | else 90 | obj.innerHTML = "Person not found :/" 91 | state.widget.inject(obj, true) 92 | -------------------------------------------------------------------------------- /ui/js/coffee/account.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | signup = (form) -> 19 | err = null 20 | if form.name.value.length < 2 21 | err = "Please enter your full name" 22 | else if form.screenname.value.length < 1 23 | err = "Please enter a screen name" 24 | else if form.email.value.length < 6 or not form.email.value.match(/^[^\r\n\s @]+@[^\r\n\s @]+$/) 25 | err = "Please enter a valid email address" 26 | else if form.password.value.length < 1 or form.password.value != form.password2.value 27 | err = "Please enter your password and make sure it matches the re-type" 28 | if err 29 | document.getElementById('signupmsg').innerHTML = "

Error: " + err + "

" 30 | return false 31 | else 32 | document.getElementById('signupmsg').innerHTML = "Creating account, hang on..!" 33 | post('user-signup', 34 | { 35 | action: 'create', 36 | name: form.name.value, 37 | password: form.password.value, 38 | screenname: form.screenname.value, 39 | email: form.email.value, 40 | code: form.code.value 41 | } 42 | , null, validateSignup) 43 | return false 44 | 45 | validateSignup = (json, state) -> 46 | if json.created 47 | document.getElementById('signupmsg').innerHTML = "Account created! Please check your inbox for verification instructions." 48 | else 49 | document.getElementById('signupmsg').innerHTML = "

Error: " + json.message + "

" 50 | 51 | login = (form) -> 52 | if form.email.value.length > 5 and form.password.value.length > 0 53 | cog(document.getElementById('loginmsg')) 54 | post('account', { 55 | username: form.email.value, 56 | password: form.password.value, 57 | api: form.api.value 58 | }, null, validateLogin) 59 | return false 60 | 61 | validateLogin = (json, state) -> 62 | if json.loginRequired 63 | document.getElementById('loginmsg').innerHTML = json.error 64 | else 65 | if json.apiversion and json.apiversion >= 3 66 | if document.referrer and document.referrer.match(/https:\/\/(?:www)?\.snoot\.io\/dashboard/i) 67 | location.href = document.referrer 68 | else 69 | location.href = "/dashboard.html?page=default" 70 | else 71 | location.href = "/api2.html?page=default" 72 | 73 | doResetPass = () -> 74 | rtoken = get('rtoken').value 75 | newpass = get('newpass').value 76 | post('account',{ remail: remail, rtoken: rtoken, newpass: newpass } , null, pwReset) 77 | return false 78 | 79 | remail = "" 80 | pwReset = () -> 81 | get('resetform').innerHTML = "Assuming you entered the right token, your password has now been reset!. Log in." 82 | 83 | getResetToken = (json, state) -> 84 | form = get('resetform') 85 | form.innerHTML = "" 86 | p = mk('p', {}, "A reset token has been sent to your email address. Please enter the reset token and your new preferred password below:") 87 | app(form, p) 88 | token = mk('input', {type: 'text', placeholder: 'Reset token', autocomplete: 'off', name: 'rtoken', id: 'rtoken'}) 89 | newpw = mk('input', {type: 'password', placeholder: 'New passord', autocomplete: 'off', name: 'newpass', id: 'newpass'}) 90 | app(form, token) 91 | app(form, mk('br')) 92 | app(form, newpw) 93 | app(form, mk('br')) 94 | btn = mk('input', { type: 'button', onclick: 'doResetPass()', value: 'Reset your password'}) 95 | form.setAttribute("onsubmit", "return doResetPass();") 96 | app(form, btn) 97 | 98 | resetpw = () -> 99 | email = get('email').value 100 | remail = email 101 | post('account',{ reset: email } , null, getResetToken) 102 | return false 103 | -------------------------------------------------------------------------------- /ui/js/coffee/charts_linechart.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | charts_linechart_stacked = (o,d) => charts_linechart(o,d,'area-spline', true) 19 | 20 | charts_linechart = (obj, data, options) -> 21 | linetype = if (options and options.linetype) then options.linetype else 'line' 22 | stacked = if (options and options.stacked) then options.stacked else false 23 | if options and options.filled and linetype == "line" 24 | linetype = "area-spline" 25 | a = 0 # Number of segments 26 | asDataArray = [] 27 | asList = [] 28 | asTypes = [] 29 | axisData = { 30 | y: { 31 | tick: { 32 | format: d3.format('s') 33 | } 34 | } 35 | } 36 | if data.timeseries and isArray(data.timeseries) 37 | dateFormat = '%Y-%m-%d' 38 | if data.histogram and data.histogram == 'quarterly' 39 | dateFormat = (x) => "Q" + ([1,2,3,4][Math.floor(x.getMonth()/3)]) + ", " + x.getFullYear() 40 | if data.histogram and data.histogram == 'monthly' 41 | dateFormat = '%b, %Y' 42 | if data.interval and data.interval == 'hour' 43 | dateFormat = '%Y-%m-%d %H:%M' 44 | if data.histogram and data.histogram == 'yearly' 45 | dateFormat = '%Y' 46 | ts = [ 47 | ['x'] 48 | ] 49 | 50 | # Get all timestamps 51 | 52 | xts = {} 53 | for el in data.timeseries 54 | axisData.x = { 55 | type: 'timeseries', 56 | tick: { 57 | format: dateFormat 58 | } 59 | } 60 | 61 | ndate = new Date(parseInt(el.date)*1000.0) 62 | ts[0].push(ndate) 63 | for k, v of el 64 | if k != 'date' 65 | if k == 'deletions' 66 | v = -v 67 | if xts[k] == undefined 68 | xts[k] = [] 69 | xts[k].push(v) 70 | # If API specifies sort order in charts, follow it 71 | keys = [] 72 | if data.sortOrder 73 | keys = data.sortOrder 74 | else 75 | keys = Object.keys(xts) 76 | 77 | for key in keys 78 | val = xts[key] 79 | xx = [key] 80 | for el in val 81 | xx.push(el) 82 | ts.push(xx) 83 | asList.push(key) 84 | asTypes[key] = linetype 85 | a++ 86 | 87 | asDataArray = ts 88 | else 89 | for k, v of (data.counts||data.phrases) 90 | # If we have an array of objects with a .count sub: 91 | if isHash(v) and v.count 92 | k = v.phrase||v.value 93 | v = v.count 94 | asList.push(k) 95 | asTypes[k] = 'bar' 96 | tmpArray = [k] 97 | if isArray(v) 98 | for dataPoint in v 99 | tmpArray.push(dataPoint) 100 | else 101 | tmpArray.push(v) 102 | asDataArray.push(tmpArray) 103 | a++ 104 | config = { 105 | bindto: obj, 106 | data: { 107 | order: null, 108 | x: if data.timeseries then 'x' else null, 109 | #xFormat: '%s', 110 | columns: asDataArray, 111 | types: asTypes, 112 | groups: if stacked then [asList] else [[]] 113 | }, 114 | axis: axisData, 115 | color: { 116 | pattern: genColors(a + 1, 0.55, 0.475, true) 117 | }, 118 | subchart: { 119 | show: false 120 | }, 121 | point: { 122 | show: false 123 | }, 124 | bar: { 125 | width: { 126 | ratio: 0.7 127 | } 128 | } 129 | tooltip: { 130 | format: { 131 | value: (val) => d3.format(',')(val) 132 | } 133 | } 134 | } 135 | c = c3.generate(config) 136 | return [c, config] 137 | -------------------------------------------------------------------------------- /api/pages/code/sloc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/code/sloc 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/Sloc' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows a breakdown of lines of code for one or more sources 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/Sloc' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows a breakdown of lines of code for one or more sources 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the SLoC renderer for Kibble 68 | """ 69 | 70 | import json 71 | 72 | def run(API, environ, indata, session): 73 | 74 | # We need to be logged in for this! 75 | if not session.user: 76 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 77 | 78 | 79 | # First, fetch the view if we have such a thing enabled 80 | viewList = [] 81 | if indata.get('view'): 82 | viewList = session.getView(indata.get('view')) 83 | if indata.get('subfilter'): 84 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 85 | 86 | 87 | # Fetch all sources for default org 88 | dOrg = session.user['defaultOrganisation'] or "apache" 89 | query = { 90 | 'query': { 91 | 'bool': { 92 | 'must': [ 93 | { 94 | 'terms': { 95 | 'type': ['git', 'svn', 'github'] 96 | } 97 | }, 98 | { 99 | 'term': { 100 | 'organisation': dOrg 101 | } 102 | } 103 | ] 104 | } 105 | } 106 | } 107 | # Source-specific or view-specific?? 108 | if indata.get('source'): 109 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 110 | elif viewList: 111 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 112 | 113 | res = session.DB.ES.search( 114 | index=session.DB.dbname, 115 | doc_type="source", 116 | size = 5000, 117 | body = query 118 | ) 119 | 120 | languages = {} 121 | years = 0 122 | for hit in res['hits']['hits']: 123 | doc = hit['_source'] 124 | if 'sloc' in doc: 125 | sloc = doc['sloc'] 126 | years += sloc['years'] 127 | for k, v in sloc['languages'].items(): 128 | languages[k] = languages.get(k, {'code': 0, 'comment': 0, 'blank': 0}) 129 | languages[k]['code'] += v.get('code', 0) 130 | languages[k]['comment'] += v.get('comment', 0) 131 | languages[k]['blank'] += v.get('blank', 0) 132 | 133 | 134 | JSON_OUT = { 135 | 'languages': languages, 136 | 'okay': True, 137 | 'years': years 138 | } 139 | yield json.dumps(JSON_OUT) 140 | -------------------------------------------------------------------------------- /ui/js/coffee/widget_messages.coffee: -------------------------------------------------------------------------------- 1 | messages = (json, state) -> 2 | 3 | if isArray json 4 | obj = document.createElement('form') 5 | 6 | tbl = mk('table') 7 | set(tbl, 'class', 'table table-striped') 8 | thead = mk('thead') 9 | tr = mk('tr') 10 | for el in ['Date', 'Sender', 'Subject'] 11 | td = mk('th') 12 | if el.match(/(Date|Sender)/) 13 | td.style.width = "20%" 14 | app(td, txt(el)) 15 | app(tr, td) 16 | app(thead, tr) 17 | app(tbl, thead) 18 | 19 | tbody = mk('tbody') 20 | app(tbl, tbody) 21 | 22 | for message in json 23 | tr = mk('tr') 24 | if message.read == false 25 | tr.style.fontWeight = "bold" 26 | tr.style.color = "#396" 27 | td = mk('td') 28 | a = mk('a') 29 | set(a, 'href', '?page=messages&message=' + message.id) 30 | app(a, txt(new Date(message.epoch*1000).toString())) 31 | app(td, a) 32 | app(tr, td) 33 | 34 | td = mk('td') 35 | a = mk('a') 36 | set(a, 'href', '?page=messages&message=' + message.id) 37 | app(a, txt(message.senderName)) 38 | app(td, a) 39 | app(tr, td) 40 | 41 | td = mk('td') 42 | a = mk('a') 43 | set(a, 'href', '?page=messages&message=' + message.id) 44 | app(a, txt(message.subject)) 45 | app(td, a) 46 | app(tr, td) 47 | app(tbody, tr) 48 | 49 | app(obj, tbl) 50 | 51 | items = 52 | recipient: 'Recipient ID' 53 | subject: "Message subject" 54 | body: "Message" 55 | 56 | h2 = mk('h2') 57 | app(h2, txt("Send a message:")) 58 | app(obj, h2) 59 | 60 | for item in ['recipient', 'subject', 'body'] 61 | div = mk('div') 62 | app(div, txt(items[item] + ": ")) 63 | if item == 'body' 64 | inp = mk('textarea') 65 | inp.style.width = "600px" 66 | inp.style.height = "200px" 67 | else 68 | inp = mk('input') 69 | set(inp, 'type', 'text') 70 | inp.style.width = "200px" 71 | set(inp, 'name', item) 72 | app(div, inp) 73 | app(obj, div) 74 | 75 | btn = mk('input') 76 | set(btn, 'type', 'button') 77 | set(btn, 'onclick', 'sendEmail(this.form)') 78 | set(btn, 'value', "Send message") 79 | app(obj, btn) 80 | 81 | #obj.innerHTML += JSON.stringify(json) 82 | state.widget.inject(obj, true) 83 | else 84 | obj = mk('div') 85 | b = mk('b') 86 | app(b, txt("Sender: ")) 87 | app(obj, b) 88 | app(obj, txt(json.senderName + ' (' + json.sender + ')')) 89 | app(obj, mk('br')) 90 | 91 | b = mk('b') 92 | app(b, txt("Date: ")) 93 | app(obj, b) 94 | app(obj, txt(new Date(json.epoch*1000).toString())) 95 | app(obj, mk('br')) 96 | 97 | b = mk('b') 98 | app(b, txt("Subject: ")) 99 | app(obj, b) 100 | app(obj, txt(json.subject)) 101 | app(obj, mk('br')) 102 | app(obj, mk('br')) 103 | 104 | pre = mk('pre') 105 | app(pre, txt(json.body)) 106 | app(obj, pre) 107 | 108 | app(obj, mk('hr')) 109 | 110 | form = mk('form') 111 | items = 112 | recipient: 'Recipient ID' 113 | subject: "Message subject" 114 | body: "Message" 115 | 116 | h2 = mk('h2') 117 | app(h2, txt("Send a reply:")) 118 | app(form, h2) 119 | 120 | reply = { 121 | recipient: json.sender 122 | subject: 'RE: ' + json.subject 123 | body: '' 124 | } 125 | 126 | for item in ['recipient', 'subject', 'body'] 127 | div = mk('div') 128 | app(div, txt(items[item] + ": ")) 129 | if item == 'body' 130 | inp = mk('textarea') 131 | inp.style.width = "600px" 132 | inp.style.height = "200px" 133 | else 134 | inp = mk('input') 135 | set(inp, 'type', 'text') 136 | inp.style.width = "200px" 137 | inp.value = reply[item] 138 | set(inp, 'name', item) 139 | app(div, inp) 140 | app(form, div) 141 | 142 | btn = mk('input') 143 | set(btn, 'type', 'button') 144 | set(btn, 'onclick', 'sendEmail(this.form)') 145 | set(btn, 'value', "Send message") 146 | app(form, btn) 147 | 148 | app(obj, form) 149 | state.widget.inject(obj, true) 150 | 151 | sendEmail = (form) -> 152 | js = { 153 | action: 'send' 154 | } 155 | for i in [0..form.length-1] 156 | k = form[i].name 157 | v = form[i].value 158 | if k in ['recipient', 'subject', 'body'] 159 | js[k] = v 160 | postJSON("messages", js, null, (a) -> alert("Mail sent!") ) 161 | -------------------------------------------------------------------------------- /setup/mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": { 3 | "_doc": { 4 | "properties": { 5 | "@version": { 6 | "type": "long" 7 | }, 8 | "address": { 9 | "type": "keyword" 10 | }, 11 | "admins": { 12 | "type": "text" 13 | }, 14 | "assignee": { 15 | "type": "keyword" 16 | }, 17 | "author_email": { 18 | "type": "keyword" 19 | }, 20 | "author_name": { 21 | "type": "keyword" 22 | }, 23 | "authors": { 24 | "type": "long" 25 | }, 26 | "blank": { 27 | "type": "long" 28 | }, 29 | "category": { 30 | "type": "keyword" 31 | }, 32 | "changeDate": { 33 | "format": "yyyy/MM/dd HH:mm:ss", 34 | "store": true, 35 | "type": "date" 36 | }, 37 | "closed": { 38 | "type": "double" 39 | }, 40 | "closedDate": { 41 | "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis", 42 | "type": "date" 43 | }, 44 | "comments": { 45 | "type": "long" 46 | }, 47 | "committer_email": { 48 | "type": "keyword" 49 | }, 50 | "committer_name": { 51 | "type": "keyword" 52 | }, 53 | "cost": { 54 | "type": "double" 55 | }, 56 | "created": { 57 | "type": "double" 58 | }, 59 | "creator": { 60 | "type": "keyword" 61 | }, 62 | "createdDate": { 63 | "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis", 64 | "type": "date" 65 | }, 66 | "date": { 67 | "format": "yyyy/MM/dd HH:mm:ss", 68 | "store": true, 69 | "type": "date" 70 | }, 71 | "defaultOrganisation": { 72 | "type": "keyword" 73 | }, 74 | "default_branch": { 75 | "type": "text" 76 | }, 77 | "deletions": { 78 | "type": "long" 79 | }, 80 | "displayName": { 81 | "type": "text" 82 | }, 83 | "email": { 84 | "type": "keyword" 85 | }, 86 | "emails": { 87 | "type": "long" 88 | }, 89 | "exception": { 90 | "type": "text" 91 | }, 92 | "good": { 93 | "type": "boolean" 94 | }, 95 | "hash": { 96 | "type": "keyword" 97 | }, 98 | "id": { 99 | "type": "keyword" 100 | }, 101 | "insertions": { 102 | "type": "long" 103 | }, 104 | "issueCloser": { 105 | "type": "keyword" 106 | }, 107 | "issueCreator": { 108 | "type": "keyword" 109 | }, 110 | "key": { 111 | "type": "keyword" 112 | }, 113 | "labels": { 114 | "type": "text" 115 | }, 116 | "loc": { 117 | "type": "long" 118 | }, 119 | "name": { 120 | "type": "keyword" 121 | }, 122 | "organisation": { 123 | "type": "keyword" 124 | }, 125 | "organisations": { 126 | "type": "text" 127 | }, 128 | "ownerships": { 129 | "type": "text" 130 | }, 131 | "password": { 132 | "type": "keyword" 133 | }, 134 | "request_id": { 135 | "type": "keyword" 136 | }, 137 | "sender": { 138 | "type": "keyword" 139 | }, 140 | "shash": { 141 | "type": "keyword" 142 | }, 143 | "source": { 144 | "type": "keyword" 145 | }, 146 | "sourceID": { 147 | "type": "keyword" 148 | }, 149 | "sourceURL": { 150 | "type": "keyword" 151 | }, 152 | "status": { 153 | "type": "keyword" 154 | }, 155 | "subject": { 156 | "type": "text" 157 | }, 158 | "sync": { 159 | "type": "double" 160 | }, 161 | "tag": { 162 | "type": "keyword" 163 | }, 164 | "text": { 165 | "type": "text" 166 | }, 167 | "time": { 168 | "type": "double" 169 | }, 170 | "title": { 171 | "type": "text" 172 | }, 173 | "topics": { 174 | "type": "long" 175 | }, 176 | "ts": { 177 | "type": "long" 178 | }, 179 | "tsday": { 180 | "type": "long" 181 | }, 182 | "type": { 183 | "type": "keyword" 184 | }, 185 | "url": { 186 | "type": "keyword" 187 | }, 188 | "vcs": { 189 | "type": "keyword" 190 | }, 191 | "verified": { 192 | "type": "boolean" 193 | }, 194 | "years": { 195 | "type": "double" 196 | } 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /api/pages/code/top-sloc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/code/top-sloc 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/Timeseries' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows top 25 repos by lines of code 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/Timeseries' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows top 25 repos by lines of code 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the TopN repos by SLoC list renderer for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import re 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | #################################################################### 91 | #################################################################### 92 | dOrg = session.user['defaultOrganisation'] or "apache" 93 | query = { 94 | 'query': { 95 | 'bool': { 96 | 'must': [ 97 | {'terms': 98 | { 99 | 'type': ['git', 'svn', 'github'] 100 | } 101 | }, 102 | { 103 | 'term': { 104 | 'organisation': dOrg 105 | } 106 | } 107 | ] 108 | } 109 | } 110 | } 111 | # Source-specific or view-specific?? 112 | if indata.get('source'): 113 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 114 | elif viewList: 115 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 116 | 117 | res = session.DB.ES.search( 118 | index=session.DB.dbname, 119 | doc_type="source", 120 | size = 5000, 121 | body = query 122 | ) 123 | 124 | toprepos = [] 125 | for doc in res['hits']['hits']: 126 | repo = doc['_source'] 127 | url = re.sub(r".+/([^/]+?)(?:\.git)?$", r"\1", repo['sourceURL']) 128 | if 'sloc' in repo: 129 | count = repo['sloc'].get('loc', 0) 130 | if not count: 131 | count = 0 132 | toprepos.append([url, count]) 133 | 134 | toprepos = sorted(toprepos, key = lambda x: int(x[1]), reverse = True) 135 | top = toprepos[0:24] 136 | if len(toprepos) > 25: 137 | count = 0 138 | for repo in toprepos[25:]: 139 | count += repo[1] 140 | top.append(["Other repos", count]) 141 | 142 | tophash = {} 143 | for v in top: 144 | tophash[v[0]] = v[1] 145 | 146 | JSON_OUT = { 147 | 'counts': tophash, 148 | 'okay': True, 149 | 'responseTime': time.time() - now, 150 | } 151 | yield json.dumps(JSON_OUT) 152 | -------------------------------------------------------------------------------- /api/pages/mail/top-topics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/mail/top-topics 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/CommitterList' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows the top N of email authors 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/CommitterList' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows the top N of email authors 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the TopN committers list renderer for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import hashlib 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | dateTo = indata.get('to', int(time.time())) 91 | dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span 92 | 93 | interval = indata.get('interval', 'month') 94 | 95 | 96 | #################################################################### 97 | #################################################################### 98 | dOrg = session.user['defaultOrganisation'] or "apache" 99 | query = { 100 | 'query': { 101 | 'bool': { 102 | 'must': [ 103 | {'range': 104 | { 105 | 'ts': { 106 | 'from': dateFrom, 107 | 'to': dateTo 108 | } 109 | } 110 | }, 111 | { 112 | 'term': { 113 | 'organisation': dOrg 114 | } 115 | } 116 | ] 117 | } 118 | }, 119 | 'sort': [{ 120 | 'emails': 'desc' 121 | }] 122 | } 123 | # Source-specific or view-specific?? 124 | if indata.get('source'): 125 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 126 | elif viewList: 127 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 128 | 129 | res = session.DB.ES.search( 130 | index=session.DB.dbname, 131 | doc_type="mailtop", 132 | size = 25, 133 | body = query 134 | ) 135 | 136 | topN = [] 137 | for bucket in res['hits']['hits']: 138 | topN.append( { 139 | 'source': bucket['_source']['sourceURL'], 140 | 'name': bucket['_source']['subject'], 141 | 'count': bucket['_source']['emails'] 142 | }) 143 | 144 | JSON_OUT = { 145 | 'topN': { 146 | 'denoter': 'emails', 147 | 'items': topN, 148 | 'icon': 'envelope' 149 | }, 150 | 'okay': True, 151 | 'responseTime': time.time() - now 152 | } 153 | yield json.dumps(JSON_OUT) 154 | -------------------------------------------------------------------------------- /api/pages/mail/keyphrases.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/mail/keyphrases 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/PhraseList' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows the common key phrases in use on one or more mailing lists 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/PhraseList' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows the common key phrases in use on one or more mailing lists 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the common key phrases renderer for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import hashlib 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | dateTo = indata.get('to', int(time.time())) 91 | dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span 92 | 93 | interval = indata.get('interval', 'month') 94 | 95 | 96 | #################################################################### 97 | #################################################################### 98 | dOrg = session.user['defaultOrganisation'] or "apache" 99 | query = { 100 | 'query': { 101 | 'bool': { 102 | 'must': [ 103 | {'range': 104 | { 105 | 'ts': { 106 | 'from': dateFrom, 107 | 'to': dateTo 108 | } 109 | } 110 | }, 111 | { 112 | 'term': { 113 | 'organisation': dOrg 114 | } 115 | } 116 | ] 117 | } 118 | }, 119 | 'aggs': { 120 | 'kpe': { 121 | 'terms': { 122 | 'field': 'kpe.keyword', 123 | 'size': 50 124 | } 125 | } 126 | } 127 | } 128 | # Source-specific or view-specific?? 129 | if indata.get('source'): 130 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 131 | elif viewList: 132 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 133 | 134 | res = session.DB.ES.search( 135 | index=session.DB.dbname, 136 | doc_type="email", 137 | size = 0, 138 | body = query 139 | ) 140 | 141 | topN = [] 142 | for bucket in res['aggregations']['kpe']['buckets']: 143 | topN.append( { 144 | 'phrase': bucket['key'], 145 | 'count': bucket['doc_count'] 146 | }) 147 | 148 | JSON_OUT = { 149 | 'widgetType': { 150 | 'chartType': 'bar' 151 | }, 152 | 'phrases': topN, 153 | 'okay': True, 154 | 'responseTime': time.time() - now 155 | } 156 | yield json.dumps(JSON_OUT) 157 | -------------------------------------------------------------------------------- /api/pages/issue/age.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/issue/age 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/Timeseries' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows timeseries of no. of open tickets by age 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/Timeseries' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows timeseries of no. of open tickets by age 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the issue actors stats page for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import hashlib 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | interval = indata.get('interval', 'month') 91 | 92 | #################################################################### 93 | #################################################################### 94 | dOrg = session.user['defaultOrganisation'] or "apache" 95 | query = { 96 | 'query': { 97 | 'bool': { 98 | 'must': [ 99 | { 100 | 'term': { 101 | 'status': 'open' 102 | } 103 | }, 104 | { 105 | 'term': { 106 | 'organisation': dOrg 107 | } 108 | } 109 | ] 110 | } 111 | } 112 | } 113 | # Source-specific or view-specific?? 114 | if indata.get('source'): 115 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 116 | elif viewList: 117 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 118 | if indata.get('email'): 119 | query['query']['bool']['should'] = [{'term': {'issueCreator': indata.get('email')}}, {'term': {'issueCloser': indata.get('email')}}] 120 | query['query']['bool']['minimum_should_match'] = 1 121 | 122 | # Get timeseries for this period 123 | query['aggs'] = { 124 | 'per_interval': { 125 | 'date_histogram': { 126 | 'field': 'createdDate', 127 | 'interval': interval 128 | } 129 | } 130 | } 131 | 132 | res = session.DB.ES.search( 133 | index=session.DB.dbname, 134 | doc_type="issue", 135 | size = 0, 136 | body = query 137 | ) 138 | timeseries = [] 139 | opened = 0 140 | for bucket in res['aggregations']['per_interval']['buckets']: 141 | ts = int(bucket['key'] / 1000) 142 | opened += bucket['doc_count'] 143 | timeseries.append( { 144 | 'date': ts, 145 | 'open': opened 146 | }) 147 | 148 | 149 | 150 | JSON_OUT = { 151 | 'timeseries': timeseries, 152 | 'okay': True, 153 | 'responseTime': time.time() - now, 154 | 'widgetType': { 155 | 'chartType': 'line' 156 | } 157 | } 158 | yield json.dumps(JSON_OUT) 159 | -------------------------------------------------------------------------------- /api/pages/forum/top.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/forum/top 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/TopList' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows the top N issues by interactions 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/TopList' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows the top N topics by interactions 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the issue actors stats page for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import hashlib 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | dateTo = indata.get('to', int(time.time())) 91 | dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span 92 | 93 | interval = indata.get('interval', 'month') 94 | 95 | 96 | #################################################################### 97 | #################################################################### 98 | dOrg = session.user['defaultOrganisation'] or "apache" 99 | query = { 100 | 'query': { 101 | 'bool': { 102 | 'must': [ 103 | {'range': 104 | { 105 | 'created': { 106 | 'from': dateFrom, 107 | 'to': dateTo 108 | } 109 | } 110 | }, 111 | { 112 | 'term': { 113 | 'organisation': dOrg 114 | } 115 | } 116 | ] 117 | } 118 | }, 119 | 'sort': { 120 | 'posts': 'desc' 121 | } 122 | } 123 | # Source-specific or view-specific?? 124 | if indata.get('source'): 125 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 126 | elif viewList: 127 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 128 | if indata.get('email'): 129 | query['query']['bool']['should'] = [{'term': {'creator': indata.get('email')}}] 130 | 131 | res = session.DB.ES.search( 132 | index=session.DB.dbname, 133 | doc_type="forum_topic", 134 | size = 25, 135 | body = query 136 | ) 137 | top = [] 138 | for bucket in res['hits']['hits']: 139 | doc = bucket['_source'] 140 | doc['source'] = doc.get('url', '#') 141 | doc['name'] = doc.get('type', 'unknown') 142 | doc['subject'] = doc.get('title') 143 | doc['count'] = doc.get('posts', 0) 144 | top.append(doc) 145 | 146 | 147 | JSON_OUT = { 148 | 'topN': { 149 | 'denoter': 'interactions', 150 | 'icon': 'comment', 151 | 'items': top 152 | }, 153 | 'okay': True, 154 | 'responseTime': time.time() - now, 155 | 'widgetType': { 156 | 'chartType': 'line' 157 | } 158 | } 159 | yield json.dumps(JSON_OUT) 160 | -------------------------------------------------------------------------------- /api/pages/issue/top.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Licensed to the Apache Software Foundation (ASF) under one or more 4 | # contributor license agreements. See the NOTICE file distributed with 5 | # this work for additional information regarding copyright ownership. 6 | # The ASF licenses this file to You under the Apache License, Version 2.0 7 | # (the "License"); you may not use this file except in compliance with 8 | # the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ######################################################################## 18 | # OPENAPI-URI: /api/issue/top 19 | ######################################################################## 20 | # get: 21 | # responses: 22 | # '200': 23 | # content: 24 | # application/json: 25 | # schema: 26 | # $ref: '#/components/schemas/TopList' 27 | # description: 200 Response 28 | # default: 29 | # content: 30 | # application/json: 31 | # schema: 32 | # $ref: '#/components/schemas/Error' 33 | # description: unexpected error 34 | # security: 35 | # - cookieAuth: [] 36 | # summary: Shows the top N issues by interactions 37 | # post: 38 | # requestBody: 39 | # content: 40 | # application/json: 41 | # schema: 42 | # $ref: '#/components/schemas/defaultWidgetArgs' 43 | # responses: 44 | # '200': 45 | # content: 46 | # application/json: 47 | # schema: 48 | # $ref: '#/components/schemas/TopList' 49 | # description: 200 Response 50 | # default: 51 | # content: 52 | # application/json: 53 | # schema: 54 | # $ref: '#/components/schemas/Error' 55 | # description: unexpected error 56 | # security: 57 | # - cookieAuth: [] 58 | # summary: Shows the top N issues by interactions 59 | # 60 | ######################################################################## 61 | 62 | 63 | 64 | 65 | 66 | """ 67 | This is the issue actors stats page for Kibble 68 | """ 69 | 70 | import json 71 | import time 72 | import hashlib 73 | 74 | def run(API, environ, indata, session): 75 | 76 | # We need to be logged in for this! 77 | if not session.user: 78 | raise API.exception(403, "You must be logged in to use this API endpoint! %s") 79 | 80 | now = time.time() 81 | 82 | # First, fetch the view if we have such a thing enabled 83 | viewList = [] 84 | if indata.get('view'): 85 | viewList = session.getView(indata.get('view')) 86 | if indata.get('subfilter'): 87 | viewList = session.subFilter(indata.get('subfilter'), view = viewList) 88 | 89 | 90 | dateTo = indata.get('to', int(time.time())) 91 | dateFrom = indata.get('from', dateTo - (86400*30*6)) # Default to a 6 month span 92 | 93 | interval = indata.get('interval', 'month') 94 | 95 | 96 | #################################################################### 97 | #################################################################### 98 | dOrg = session.user['defaultOrganisation'] or "apache" 99 | query = { 100 | 'query': { 101 | 'bool': { 102 | 'must': [ 103 | {'range': 104 | { 105 | 'created': { 106 | 'from': dateFrom, 107 | 'to': dateTo 108 | } 109 | } 110 | }, 111 | { 112 | 'term': { 113 | 'organisation': dOrg 114 | } 115 | } 116 | ] 117 | } 118 | }, 119 | 'sort': { 120 | 'comments': 'desc' 121 | } 122 | } 123 | # Source-specific or view-specific?? 124 | if indata.get('source'): 125 | query['query']['bool']['must'].append({'term': {'sourceID': indata.get('source')}}) 126 | elif viewList: 127 | query['query']['bool']['must'].append({'terms': {'sourceID': viewList}}) 128 | if indata.get('email'): 129 | query['query']['bool']['should'] = [{'term': {'issueCreator': indata.get('email')}}, {'term': {'issueCloser': indata.get('email')}}] 130 | query['query']['bool']['minimum_should_match'] = 1 131 | 132 | res = session.DB.ES.search( 133 | index=session.DB.dbname, 134 | doc_type="issue", 135 | size = 25, 136 | body = query 137 | ) 138 | top = [] 139 | for bucket in res['hits']['hits']: 140 | doc = bucket['_source'] 141 | doc['source'] = doc.get('url', '#') 142 | doc['name'] = doc.get('key', 'unknown') 143 | doc['subject'] = doc.get('title') 144 | doc['count'] = doc.get('comments', 0) 145 | top.append(doc) 146 | 147 | 148 | JSON_OUT = { 149 | 'topN': { 150 | 'denoter': 'interactions', 151 | 'icon': 'bug', 152 | 'items': top 153 | }, 154 | 'okay': True, 155 | 'responseTime': time.time() - now, 156 | 'widgetType': { 157 | 'chartType': 'line' 158 | } 159 | } 160 | yield json.dumps(JSON_OUT) 161 | --------------------------------------------------------------------------------