зеркало из https://github.com/mozilla/code-review.git
static-analysis/frontend: Import existing project (#1340)
This commit is contained in:
Коммит
c55338acc6
|
@ -0,0 +1,48 @@
|
|||
fs = require('fs');
|
||||
|
||||
const envs = {
|
||||
CONFIG: process.env.CONFIG || 'staging',
|
||||
};
|
||||
const PORT = process.env.PORT || 8010;
|
||||
// HTTPS can be disabled by setting HTTPS_DISABLED environment variable to
|
||||
// true. Otherwise it will enforced either using automatically generated
|
||||
// certificates or pre-generated ones.
|
||||
const HTTPS = process.env.HTTPS_DISABLED ? false :
|
||||
(process.env.SSL_CERT && process.env.SSL_KEY && process.env.SSL_CACERT) ?
|
||||
{
|
||||
cert: fs.readFileSync(process.env.SSL_CERT),
|
||||
key: fs.readFileSync(process.env.SSL_KEY),
|
||||
ca: fs.readFileSync(process.env.SSL_CACERT)
|
||||
}
|
||||
: true;
|
||||
|
||||
// Set environment variables to their default values if not defined
|
||||
Object.keys(envs).forEach(env => !(env in process.env) && (process.env[env] = envs[env]));
|
||||
|
||||
module.exports = {
|
||||
use: [
|
||||
'@neutrinojs/standardjs',
|
||||
[
|
||||
'@neutrinojs/vue',
|
||||
{
|
||||
html: {
|
||||
title: 'Mozilla Static Analysis'
|
||||
},
|
||||
devServer: {
|
||||
port: PORT,
|
||||
https: HTTPS,
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: '__heartbeat__', to: 'views/ok.html' },
|
||||
{ from: '__lbheartbeat__', to: 'views/ok.html' },
|
||||
{ from: '__version__', to: 'views/version.json' },
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
'@neutrinojs/mocha',
|
||||
['@neutrinojs/env', Object.keys(envs)],
|
||||
]
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{ releng_pkgs
|
||||
}:
|
||||
|
||||
releng_pkgs.lib.mkYarnFrontend {
|
||||
src = ./.;
|
||||
src_path = ./.;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "static-analysis-frontend",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/mozilla/release-services",
|
||||
"author": "Bastien Abadie <babadie@mozilla.com>",
|
||||
"description": "Mozilla static analysis frontend",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"bulma": "^0.7.1",
|
||||
"vue": "^2.5.16",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@neutrinojs/mocha": "^8.3.0",
|
||||
"@neutrinojs/standardjs": "^8.3.0",
|
||||
"@neutrinojs/vue": "^8.3.0",
|
||||
"neutrino": "^8.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "neutrino build",
|
||||
"start": "neutrino start",
|
||||
"lint": "neutrino lint",
|
||||
"test": "neutrino test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MPLv2"
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import Tasks from './Tasks.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Tasks
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
channels: ['testing', 'staging', 'production']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switch_channel (channel) {
|
||||
this.$store.dispatch('switch_channel', channel)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
channel () {
|
||||
return this.$store.state.channel
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<main>
|
||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||
<div class="container is-fluid">
|
||||
<div class="navbar-brand">
|
||||
<div class="navbar-item">Static analysis</div>
|
||||
</div>
|
||||
<div class="navbar-menu">
|
||||
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<span class="navbar-link">{{ channel }}</span>
|
||||
<div class="navbar-dropdown is-boxed">
|
||||
<a class="dropdown-item" v-for="c in channels" :class="{'is-active': c == channel}" v-on:click="switch_channel(c)">
|
||||
{{ c }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item" v-if="$route.name != 'stats'">
|
||||
<router-link to="/stats" class="button is-link">All checks</router-link>
|
||||
</div>
|
||||
<div class="navbar-item" v-if="$route.name != 'tasks'">
|
||||
<router-link to="/" class="button is-link">All tasks</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container is-fluid">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Built by <a href="https://wiki.mozilla.org/Release_Management" target="_blank">Release Management team</a>
|
||||
<span>•</span>
|
||||
<a href="https://github.com/La0/mozilla-static-analysis" target="_blank">Source Code</a>
|
||||
<span>•</span>
|
||||
<a href="https://github.com/La0/mozilla-static-analysis/issues" target="_blank">Report an issue</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.navbar-brand .navbar-item {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: #a3cc69 !important;
|
||||
}
|
||||
|
||||
div.navbar-item.has-dropdown {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* Bottom footer support, it's not native in Bulma :( */
|
||||
div#app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div#app main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
div#app footer {
|
||||
border-top: 1px solid #CCC;
|
||||
padding: 2px;
|
||||
font-size: 0.9em;
|
||||
color: #444;
|
||||
background: #EEE;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div#app footer a:hover {
|
||||
text-decoration: underline;
|
||||
color: #3273dc;
|
||||
}
|
||||
|
||||
div#app footer span {
|
||||
color: #CCC;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,13 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'Bool',
|
||||
props: ['value', 'name']
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span v-if="value" class="has-text-success">✔ {{ name }}</span>
|
||||
<span v-else class="has-text-danger">✕ {{ name }}</span>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import mixins from './mixins.js'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
mixins.stats
|
||||
],
|
||||
computed: {
|
||||
check_name () {
|
||||
return this.$route.params.check
|
||||
},
|
||||
check () {
|
||||
if (!this.stats || !this.stats.loaded) {
|
||||
return null
|
||||
}
|
||||
// Shallow clone to trigger Vue js reactivity on deeply nested objects
|
||||
return Object.assign({}, this.$store.state.stats.checks[this.check_name])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="title">Check {{ check_name }}</h1>
|
||||
<h2 class="subtitle" v-if="stats && stats.ids">Loaded {{ stats.loaded }}/{{ stats.ids.length }} tasks</h2>
|
||||
|
||||
<div v-if="stats">
|
||||
<progress class="progress is-info" :class="{'is-info': progress < 100, 'is-success': progress >= 100}" :value="progress" max="100">{{ progress }}%</progress>
|
||||
|
||||
<table class="table is-fullwidth" v-if="check">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>Review</th>
|
||||
<th>Path</th>
|
||||
<th>Line</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="issue in check.issues">
|
||||
<td>
|
||||
<a class="mono" :href="'https://tools.taskcluster.net/task-inspector/#' + issue.taskId" target="_blank">{{ issue.taskId }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="issue.revision.url" target="_blank" v-if="issue.revision.source == 'phabricator'">Phabricator {{ issue.revision.id }}</a>
|
||||
<a :href="issue.revision.url" target="_blank" v-else-if="issue.revision.source == 'mozreview'">Mozreview {{ issue.revision.review_reques }}</a>
|
||||
<span v-else>Unknown</span>
|
||||
</td>
|
||||
<td class="mono">{{ issue.path }}</td>
|
||||
<td>{{ issue.line }}</td>
|
||||
<td>
|
||||
{{ issue.message }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<p v-else class="notification is-warning">No data available for this check</p>
|
||||
</div>
|
||||
<div class="notification is-info" v-else>Loading tasks...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.mono{
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
<script>
|
||||
import mixins from './mixins.js'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
mixins.stats,
|
||||
mixins.date
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
sort: 'detected'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checks () {
|
||||
if (!this.stats || !this.stats.loaded) {
|
||||
return null
|
||||
}
|
||||
let sortStr = (x, y) => x.toLowerCase().localeCompare(y.toLowerCase())
|
||||
var sorts = {
|
||||
'analyzer': (x, y) => sortStr(x.analyzer, y.analyzer) || sortStr(x.check, y.check),
|
||||
'check': (x, y) => sortStr(x.check, y.check),
|
||||
'detected': (x, y) => y.total - x.total,
|
||||
'published': (x, y) => y.publishable - x.publishable
|
||||
}
|
||||
|
||||
// Apply local sort to the checks from store
|
||||
var checks = Object.values(this.stats.checks)
|
||||
checks.sort(sorts[this.sort])
|
||||
return checks
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select_sort (name) {
|
||||
this.$set(this, 'sort', name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="title">Statistics</h1>
|
||||
<h2 class="subtitle" v-if="stats && stats.ids">
|
||||
<span>Loaded {{ stats.loaded }}/{{ stats.ids.length }} tasks with issues</span>
|
||||
<span v-if="stats && stats.start_date" :title="stats.start_date">, since {{ stats.start_date|since }} ago</span>
|
||||
</h2>
|
||||
|
||||
<div v-if="stats">
|
||||
<progress class="progress is-info" :class="{'is-info': progress < 100, 'is-success': progress >= 100}" :value="progress" max="100">{{ progress }}%</progress>
|
||||
|
||||
<table class="table is-fullwidth" v-if="checks">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="button" v-on:click="select_sort('analyzer')" :class="{'is-focused': sort == 'analyzer' }">Analyzer</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="button" v-on:click="select_sort('check')" :class="{'is-focused': sort == 'check' }">Check</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="button" v-on:click="select_sort('detected')" :class="{'is-focused': sort == 'detected' }">Detected</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="button" v-on:click="select_sort('published')" :class="{'is-focused': sort == 'published' }">Published</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="check in checks" :class="{'publishable': check.publishable > 0}">
|
||||
<td>{{ check.analyzer }}</td>
|
||||
<td>
|
||||
{{ check.check }}
|
||||
<span class="has-text-grey" v-if="check.analyzer == 'mozlint.flake8'">{{ check.message }}</span>
|
||||
</td>
|
||||
<td>{{ check.total }}</td>
|
||||
<td>
|
||||
<router-link v-if="check.publishable > 0" :to="{ name: 'check', params: { check: check.key }}">{{ check.publishable }}</router-link>
|
||||
<span class="has-text-grey" v-else>0</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="notification is-info" v-else>Loading tasks...</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
tr.publishable {
|
||||
background: #e6ffcc;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,155 @@
|
|||
<script>
|
||||
import Bool from './Bool.vue'
|
||||
export default {
|
||||
name: 'Task',
|
||||
data () {
|
||||
return {
|
||||
state: 'loading'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Bool
|
||||
},
|
||||
mounted () {
|
||||
var report = this.$store.dispatch('load_report', this.$route.params.taskId)
|
||||
report.then(
|
||||
(response) => {
|
||||
this.$set(this, 'state', 'loaded')
|
||||
},
|
||||
(error) => {
|
||||
this.$set(this, 'state', error.response.status === 404 ? 'missing' : 'error')
|
||||
}
|
||||
)
|
||||
},
|
||||
computed: {
|
||||
report () {
|
||||
return this.$store.state.report
|
||||
},
|
||||
nb_publishable () {
|
||||
if (!this.report || !this.report.issues) {
|
||||
return 0
|
||||
}
|
||||
return this.report.issues.filter(i => i.publishable).length
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
from_timestamp (value) {
|
||||
return new Date(value * 1000).toUTCString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="title">Task <a :href="'https://tools.taskcluster.net/task-inspector/#' + $route.params.taskId" target="_blank">{{ $route.params.taskId }}</a></h1>
|
||||
|
||||
<div class="notification is-info" v-if="state == 'loading'">Loading report...</div>
|
||||
<div class="notification is-warning" v-else-if="state == 'missing'">No report, so no issues !</div>
|
||||
<div class="notification is-danger" v-else-if="state == 'error'">Failure</div>
|
||||
<div v-else>
|
||||
|
||||
<nav class="level" v-if="report">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Publishable</p>
|
||||
<p class="title">{{ nb_publishable }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Issues</p>
|
||||
<p class="title">{{ report.issues.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Source</p>
|
||||
<p class="title"><a :href="report.revision.url" target="_blank">{{ report.revision.source }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Reported</p>
|
||||
<p class="title">{{ report.time|from_timestamp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<table class="table is-fullwidth" v-if="report && report.issues">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Analyzer</td>
|
||||
<td>Path</td>
|
||||
<td>Lines</td>
|
||||
<td>Publication</td>
|
||||
<td>Check</td>
|
||||
<td>Level</td>
|
||||
<td>Message</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="issue in report.issues" :class="{'publishable': issue.publishable}">
|
||||
<td>
|
||||
<span v-if="issue.analyzer == 'mozlint'">{{ issue.linter }}<br />by Mozlint</span>
|
||||
<span v-else>{{ issue.analyzer }}</span>
|
||||
</td>
|
||||
<td class="path">{{ issue.path }}</td>
|
||||
<td>{{ issue.line }} <span v-if="issue.nb_lines > 1">→ {{ issue.line - 1 + issue.nb_lines }}</span></td>
|
||||
<td>
|
||||
<Bool :value="issue.publishable" name="Publishable" />
|
||||
<ul>
|
||||
<li><Bool :value="issue.in_patch" name="In patch" /></li>
|
||||
<li><Bool :value="issue.validates" name="Validated" /></li>
|
||||
<li><Bool :value="issue.publishable" name="New issue" /></li>
|
||||
</ul>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span v-if="issue.analyzer == 'mozlint'">{{ issue.rule }}</span>
|
||||
<span v-if="issue.analyzer == 'clang-tidy'">{{ issue.check }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="issue.level == 'error' || issue.type == 'error'" class="tag is-danger">Error</span>
|
||||
<span v-if="issue.level == 'warning' || issue.type == 'warning'" class="tag is-warning">Warning</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ issue.message }}
|
||||
<pre v-if="issue.body">
|
||||
{{ issue.body }}
|
||||
</pre>
|
||||
|
||||
<div v-if="issue.analyzer == 'clang-format' && issue.mode == 'replace'">
|
||||
<strong>Replace</strong>
|
||||
<pre>{{ issue.old_lines }}</pre>
|
||||
<strong>by these:</strong>
|
||||
<pre>{{ issue.new_lines }}</pre>
|
||||
</div>
|
||||
<div v-if="issue.analyzer == 'clang-format' && issue.mode == 'insert'">
|
||||
<strong>Insert these lines</strong>
|
||||
<pre>{{ issue.new_lines }}</pre>
|
||||
</div>
|
||||
<div v-if="issue.analyzer == 'clang-format' && issue.mode == 'delete'">
|
||||
<strong>Delete these lines</strong>
|
||||
<pre>{{ issue.old_lines }}</pre>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="notification is-info" v-else>No issues !</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
tr.publishable {
|
||||
background: #e6ffcc;
|
||||
}
|
||||
|
||||
td.path {
|
||||
color: #4d4d4d;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
<script>
|
||||
import mixins from './mixins.js'
|
||||
|
||||
export default {
|
||||
mounted () {
|
||||
this.$store.dispatch('load_all_indexes')
|
||||
},
|
||||
mixins: [
|
||||
mixins.date
|
||||
],
|
||||
computed: {
|
||||
tasks () {
|
||||
return this.$store.state.tasks
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>#</td>
|
||||
<td>Revision</td>
|
||||
<td>State</td>
|
||||
<td>Nb. Issues</td>
|
||||
<td>Indexed</td>
|
||||
<td>Actions</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="task in tasks">
|
||||
<td>
|
||||
<a class="mono" :href="'https://tools.taskcluster.net/task-inspector/#' + task.taskId" target="_blank">{{ task.taskId }}</a>
|
||||
</td>
|
||||
|
||||
<td v-if="task.data.source == 'mozreview'">
|
||||
|
||||
<span class="tag is-success">MozReview</span> #{{ task.data.review_request }}
|
||||
|
||||
<br />
|
||||
<small class="mono has-dark-text">{{ task.data.rev}}</small>
|
||||
</td>
|
||||
<td v-else-if="task.data.source == 'phabricator'">
|
||||
|
||||
<span class="tag is-dark">Phabricator</span> #{{ task.data.id }}
|
||||
|
||||
<br />
|
||||
<small class="mono has-dark-text">{{ task.data.diff_phid}}</small>
|
||||
</td>
|
||||
<td v-else>
|
||||
<p class="notification is-danger">Unknown data source: {{ task.data.source }}</p>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="tag is-light" v-if="task.data.state == 'started'">Started</span>
|
||||
<span class="tag is-info" v-else-if="task.data.state == 'cloned'">Cloned</span>
|
||||
<span class="tag is-info" v-else-if="task.data.state == 'analyzing'">Analyzing</span>
|
||||
<span class="tag is-primary" v-else-if="task.data.state == 'analyzed'">Analyzed</span>
|
||||
<span class="tag is-danger" v-else-if="task.data.state == 'error'">Error</span>
|
||||
<span class="tag is-success" v-else-if="task.data.state == 'done'">Done</span>
|
||||
<span class="tag is-black" v-else>Unknown</span>
|
||||
</td>
|
||||
|
||||
<td :class="{'has-text-success': task.data.issues_publishable > 0}">
|
||||
|
||||
<span v-if="task.data.issues_publishable > 0">{{ task.data.issues_publishable }}</span>
|
||||
<span v-else-if="task.data.issues_publishable == 0">{{ task.data.issues_publishable }}</span>
|
||||
<span v-else>-</span>
|
||||
/ {{ task.data.issues }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span :title="task.data.indexed">{{ task.data.indexed|since }} ago</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="button is-link" :href="task.data.url" target="_blank">Review</a>
|
||||
<router-link :to="{ name: 'task', params: { taskId : task.taskId }}" class="button is-primary">Details</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
a.mono{
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
import Vue from 'vue'
|
||||
import 'bulma/css/bulma.css'
|
||||
import App from './App.vue'
|
||||
import store from './store.js'
|
||||
import router from './routes.js'
|
||||
|
||||
export default new Vue({
|
||||
store,
|
||||
router,
|
||||
el: '#root',
|
||||
render: (h) => h(App),
|
||||
beforeCreate () {
|
||||
this.$store.commit('load_preferences')
|
||||
}
|
||||
})
|
|
@ -0,0 +1,44 @@
|
|||
export default {
|
||||
stats: {
|
||||
mounted () {
|
||||
this.$store.dispatch('calc_stats')
|
||||
},
|
||||
computed: {
|
||||
stats () {
|
||||
return this.$store.state.stats
|
||||
},
|
||||
progress () {
|
||||
if (!this.stats || !this.stats.ids) {
|
||||
return null
|
||||
}
|
||||
return 100 * this.stats.loaded / this.stats.ids.length
|
||||
}
|
||||
}
|
||||
},
|
||||
date: {
|
||||
filters: {
|
||||
// Display time since elapsed in a human format
|
||||
since (datetime) {
|
||||
var dspStep = (t, name) => (Math.round(t) + ' ' + name + (Math.round(t) > 1 ? 's' : ''))
|
||||
|
||||
let diff = (new Date() - new Date(datetime)) / 1000
|
||||
let steps = [
|
||||
[60, 'second'],
|
||||
[60, 'minute'],
|
||||
[24, 'hour'],
|
||||
[30, 'day']
|
||||
]
|
||||
var prev = ''
|
||||
for (let [t, name] of steps) {
|
||||
if (diff > t) {
|
||||
prev = dspStep(diff % t, name)
|
||||
diff = diff / t
|
||||
} else {
|
||||
return dspStep(diff, name) + ' ' + prev
|
||||
}
|
||||
}
|
||||
return 'Too long ago'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Tasks from './Tasks.vue'
|
||||
import Task from './Task.vue'
|
||||
import Stats from './Stats.vue'
|
||||
import Check from './Check.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
export default new VueRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'tasks',
|
||||
component: Tasks
|
||||
},
|
||||
{
|
||||
path: '/task/:taskId',
|
||||
name: 'task',
|
||||
component: Task
|
||||
},
|
||||
{
|
||||
path: '/stats',
|
||||
name: 'stats',
|
||||
component: Stats
|
||||
},
|
||||
{
|
||||
path: '/check/:check',
|
||||
name: 'check',
|
||||
component: Check
|
||||
}
|
||||
]
|
||||
})
|
|
@ -0,0 +1,179 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import axios from 'axios'
|
||||
import router from './routes'
|
||||
Vue.use(Vuex)
|
||||
|
||||
const PREFERENCES_KEY = 'mozilla-sa-dashboard'
|
||||
const TASKCLUSTER_INDEX = 'https://index.taskcluster.net/v1'
|
||||
const TASKCLUSTER_QUEUE = 'https://queue.taskcluster.net/v1'
|
||||
const TASKS_SLICE = 10
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
channel: 'production',
|
||||
tasks: [],
|
||||
report: null,
|
||||
stats: null
|
||||
},
|
||||
mutations: {
|
||||
load_preferences (state) {
|
||||
// Load prefs from local storage
|
||||
let rawPrefs = localStorage.getItem(PREFERENCES_KEY)
|
||||
if (rawPrefs) {
|
||||
let prefs = JSON.parse(rawPrefs)
|
||||
if (prefs.channel) {
|
||||
state.channel = prefs.channel
|
||||
}
|
||||
}
|
||||
},
|
||||
save_preferences (state) {
|
||||
// Save channel to preferences
|
||||
localStorage.setItem(PREFERENCES_KEY, JSON.stringify({
|
||||
channel: state.channel
|
||||
}))
|
||||
},
|
||||
use_channel (state, channel) {
|
||||
state.channel = channel
|
||||
state.stats = null
|
||||
state.report = null
|
||||
this.commit('save_preferences')
|
||||
},
|
||||
reset_tasks (state) {
|
||||
state.tasks = []
|
||||
},
|
||||
reset_stats (state) {
|
||||
// List all active tasks Ids
|
||||
var ids =
|
||||
state.tasks
|
||||
.filter((task) => task.data.state === 'done' && task.data.issues > 0)
|
||||
.map(t => t.taskId)
|
||||
|
||||
state.stats = {
|
||||
loaded: 0,
|
||||
ids: ids,
|
||||
checks: {},
|
||||
start_date: new Date()
|
||||
}
|
||||
},
|
||||
use_tasks (state, tasks) {
|
||||
// Filter tasks without extra data
|
||||
state.tasks = state.tasks.concat(
|
||||
tasks.filter(task => task.data.indexed !== undefined)
|
||||
)
|
||||
|
||||
// Sort by indexation date
|
||||
state.tasks.sort((x, y) => {
|
||||
return new Date(y.data.indexed) - new Date(x.data.indexed)
|
||||
})
|
||||
},
|
||||
use_report (state, report) {
|
||||
state.report = report
|
||||
|
||||
if (report !== null && state.stats !== null) {
|
||||
// Calc stats for this report
|
||||
// clang-format does not provide any check information
|
||||
var checks = report.issues.filter(i => i.analyzer !== 'clang-format')
|
||||
state.stats.checks = checks.reduce((stats, issue) => {
|
||||
var analyzer = issue.analyzer + (issue.analyzer === 'mozlint' ? '.' + issue.linter : '')
|
||||
var check = issue.analyzer === 'clang-tidy' ? issue.check : issue.rule
|
||||
var key = analyzer + '.' + check
|
||||
if (stats[key] === undefined) {
|
||||
stats[key] = {
|
||||
analyzer: analyzer,
|
||||
key: key,
|
||||
message: issue.message,
|
||||
check: check,
|
||||
publishable: 0,
|
||||
issues: [],
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
stats[key].publishable += issue.publishable ? 1 : 0
|
||||
stats[key].total++
|
||||
|
||||
// Save publishable issues for Check component
|
||||
// and link report data to the issue
|
||||
if (issue.publishable) {
|
||||
let extras = {
|
||||
revision: report.revision,
|
||||
taskId: report.taskId
|
||||
}
|
||||
stats[key].issues.push(Object.assign(extras, issue))
|
||||
}
|
||||
return stats
|
||||
}, state.stats.checks)
|
||||
|
||||
// Save start date
|
||||
state.stats.start_date = new Date(Math.min(new Date(report.time * 1000.0), state.stats.start_date))
|
||||
|
||||
// Mark new report loaded
|
||||
state.stats.loaded += 1
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// Switch data channel to use
|
||||
switch_channel (state, channel) {
|
||||
state.commit('use_channel', channel)
|
||||
state.dispatch('load_all_indexes')
|
||||
router.push({ name: 'tasks' })
|
||||
},
|
||||
|
||||
// Load all indexes available
|
||||
load_all_indexes (state) {
|
||||
state.commit('reset_tasks')
|
||||
|
||||
return Promise.all([
|
||||
state.dispatch('load_index', 'mozreview'),
|
||||
state.dispatch('load_index', 'phabricator')
|
||||
])
|
||||
},
|
||||
|
||||
// Load indexed tasks summary from Taskcluster
|
||||
load_index (state, namespace) {
|
||||
let url = TASKCLUSTER_INDEX + '/tasks/project.releng.services.project.' + this.state.channel + '.shipit_static_analysis.' + namespace
|
||||
return axios.get(url).then(resp => {
|
||||
state.commit('use_tasks', resp.data.tasks)
|
||||
})
|
||||
},
|
||||
|
||||
// Load the report for a given task
|
||||
load_report (state, taskId) {
|
||||
let url = TASKCLUSTER_QUEUE + '/task/' + taskId + '/artifacts/public/results/report.json'
|
||||
state.commit('use_report', null)
|
||||
return axios.get(url).then(resp => {
|
||||
state.commit('use_report', Object.assign({ taskId }, resp.data))
|
||||
})
|
||||
},
|
||||
|
||||
// Load multiple reports for stats crunching
|
||||
calc_stats (state, tasksId) {
|
||||
// Avoid multiple loads
|
||||
if (state.state.stats !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Load all indexes to get task ids
|
||||
var indexes = state.dispatch('load_all_indexes')
|
||||
indexes.then(() => {
|
||||
console.log('Start analysis')
|
||||
state.commit('reset_stats')
|
||||
|
||||
// Start processing by batches
|
||||
state.dispatch('load_report_batch', 0)
|
||||
})
|
||||
},
|
||||
load_report_batch (state, step) {
|
||||
if (step * TASKS_SLICE > state.state.stats.ids.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Slice full loading in smaller batches to avoid using too many ressources
|
||||
var slice = state.state.stats.ids.slice(step * TASKS_SLICE, (step + 1) * TASKS_SLICE)
|
||||
var batch = Promise.all(slice.map(taskId => state.dispatch('load_report', taskId)))
|
||||
batch.then(resp => console.info('Loaded batch', step))
|
||||
batch.then(resp => state.dispatch('load_report_batch', step + 1))
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
import assert from 'assert';
|
||||
|
||||
describe('simple', () => {
|
||||
it('should be sane', () => {
|
||||
assert.equal(true, !false);
|
||||
});
|
||||
});
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче