* Clean slate

* Dev server

* API calls & HTML template

* Prod build too

* Base of eslint

* running eslint

* Generated fixes

* Last lint fixes

* Remove useless comments

* Bump node image on CI

* Remove public path option for CI build

* Remove deprecated packages

* Update README
This commit is contained in:
Bastien Abadie 2022-07-06 06:12:27 +02:00 коммит произвёл GitHub
Родитель 60ea694b00
Коммит 96430dfb0b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 12770 добавлений и 13671 удалений

Просмотреть файл

@ -217,7 +217,7 @@ tasks:
deadline: {$fromNow: '1 hour'}
payload:
maxRunTime: 3600
image: node:11-alpine
image: node:16-alpine
env:
BACKEND_URL: "${backend_url}"
command:

42
frontend/.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,42 @@
module.exports = {
root: true,
extends: [
'standard',
'plugin:vue/base'
],
globals: {
process: true,
BACKEND_URL: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaFeatures: {
generators: true,
impliedStrict: true,
objectLiteralDuplicateProperties: false
},
ecmaVersion: 2017,
parser: '@babel/eslint-parser',
sourceType: 'module'
},
plugins: [
'babel',
'vue'
],
rules: {
'babel/new-cap': [
'error',
{
newIsCap: true
}
],
'babel/object-curly-spacing': [
'error',
'always'
],
'new-cap': 'off',
'object-curly-spacing': 'off'
},
settings: {},
};

Просмотреть файл

@ -1,49 +0,0 @@
fs = require('fs');
const envs = {
CONFIG: process.env.CONFIG || 'staging',
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:8000',
};
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 Code Review Bot'
},
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)],
]
};

Просмотреть файл

@ -3,11 +3,23 @@ Code Review Frontend
This is a simple Vue.JS administration frontend, the production instance is publicly available at https://code-review.moz.tools/
You'll need Node 16+ to be able to build it.
Developer setup
---------------
```
npm install
npm run build # to build once
npm run start # to start a dev server
npm run build # to build once in production mode
npm run build:dev # to build once in development mode
npm run start # to start a dev server on port 8010
```
Linting
-------
eslint is available through:
- `npm run lint` to list potential errors,
- `npm run lint:fix` to automatically fix these errors.

Просмотреть файл

@ -0,0 +1,29 @@
{
"plugins": [
"@babel/plugin-syntax-dynamic-import"
],
"presets": [
[
"@babel/preset-env",
{
"debug": false,
"exclude": [
"transform-regenerator",
"transform-async-to-generator"
],
"modules": false,
"targets": {
"browsers": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Edge versions",
"last 2 Opera versions",
"last 2 Safari versions",
"last 2 iOS versions"
]
},
"useBuiltIns": false
}
]
]
}

26015
frontend/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -9,6 +9,7 @@
"axios": "^0.27.2",
"bulma": "^0.9.4",
"chartist": "^0.11.4",
"lodash": "^4.17.21",
"vue": "^2.7.1",
"vue-chartist": "^2.3.1",
"vue-router": "^3.5.4",
@ -16,16 +17,34 @@
"vuex": "^3.6.2"
},
"devDependencies": {
"@neutrinojs/mocha": "^8.3.0",
"@neutrinojs/standardjs": "^8.3.0",
"@neutrinojs/vue": "^8.3.0",
"neutrino": "^8.3.0"
"@babel/eslint-parser": "^7.18.2",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.18.6",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-vue": "^9.1.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.6.1",
"process": "^0.11.10",
"style-loader": "^3.3.1",
"vue-loader": "^15.9.8",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0"
},
"scripts": {
"build": "neutrino build",
"start": "neutrino start",
"lint": "neutrino lint",
"test": "neutrino test"
"build": "webpack --mode=production",
"build:dev": "webpack --mode=development",
"start": "webpack serve --mode=development",
"lint": "eslint *.js src/**/*.js src/**/*.vue",
"lint:fix": "eslint --fix *.js src/**/*.js src/**/*.vue"
},
"keywords": [],
"license": "MPLv2"

Просмотреть файл

@ -3,7 +3,7 @@ export default {
name: 'App',
computed: {
backend_url () {
return this.$store.state.backend_url
return BACKEND_URL
}
}
}

Просмотреть файл

@ -25,8 +25,8 @@ import mixins from './mixins.js'
export default {
props: {
'name': String,
'choices': Array
name: String,
choices: Array
},
mixins: [
mixins.query
@ -48,7 +48,7 @@ export default {
computed: {
current () {
let current = null
let choice = this.choice || this.$route.query[this.name]
const choice = this.choice || this.$route.query[this.name]
if (choice && this.choices) {
current = this.choices.find(c => c === choice || c.value === choice)
if (!current) {

Просмотреть файл

@ -72,14 +72,14 @@ export default {
return this.$store.state.diffs.results
},
repositories () {
let repos = this.$store.state.repositories || []
const repos = this.$store.state.repositories || []
return repos.map(r => r.slug)
}
},
filters: {
treeherder_url (diff) {
let rev = diff.mercurial_hash
let tryRepo = diff.revision.repository === 'nss' ? 'nss-try' : 'try'
const rev = diff.mercurial_hash
const tryRepo = diff.revision.repository === 'nss' ? 'nss-try' : 'try'
return `https://treeherder.mozilla.org/#/jobs?repo=${tryRepo}&revision=${rev}`
},
short_repo (url) {

Просмотреть файл

@ -32,13 +32,13 @@ export default {
},
mounted () {
// Update filters from query string
let newForRevision = parseInt(this.$route.query.issue)
const newForRevision = parseInt(this.$route.query.issue)
this.filters.publishable = isNaN(newForRevision) ? null : this.choices.publishable[newForRevision]
this.filters.path = this.$route.query.path || null
this.filters.analyzer = this.$route.query.analyzer || null
// Load diff
var diff = this.$store.dispatch('load_diff', this.$route.params.diffId)
const diff = this.$store.dispatch('load_diff', this.$route.params.diffId)
diff.then(
(response) => {
this.$set(this, 'state', 'loaded')
@ -54,12 +54,12 @@ export default {
},
paths () {
// List sorted unique paths as choices
let uniquePaths = new Set(this.all_issues.map(i => i.path))
const uniquePaths = new Set(this.all_issues.map(i => i.path))
return [...uniquePaths].sort()
},
analyzers () {
// List sorted unique analyzers as choices
let uniqueAnalyzers = new Set(this.all_issues.map(i => i.analyzer))
const uniqueAnalyzers = new Set(this.all_issues.map(i => i.analyzer))
return [...uniqueAnalyzers].sort()
},
nb_publishable () {

Просмотреть файл

@ -16,10 +16,10 @@ export default {
},
paths () {
// Load all issues
let paths = new Set()
for (let diff of this.revision.diffs) {
let issues = this.$store.state.issues[diff.id] || []
for (let issue of issues) {
const paths = new Set()
for (const diff of this.revision.diffs) {
const issues = this.$store.state.issues[diff.id] || []
for (const issue of issues) {
paths.add(issue.path)
}
}
@ -29,7 +29,7 @@ export default {
},
methods: {
path_issues (diffId, path) {
let issues = this.$store.state.issues[diffId] || []
const issues = this.$store.state.issues[diffId] || []
return issues.filter(issue => issue.path === path)
}
}

Просмотреть файл

@ -17,7 +17,7 @@ export default {
return {
// Data filters
since: since,
since,
analyzer: null,
repository: null,
check: null,
@ -45,7 +45,7 @@ export default {
},
methods: {
load (reset) {
let payload = {}
const payload = {}
if (reset === true || this.since === '') {
this.$set(this, 'since', null)
} else {

Просмотреть файл

@ -15,7 +15,7 @@ export default {
}
},
components: {
Choice: Choice
Choice
},
data: function () {
return {
@ -69,7 +69,7 @@ export default {
// Filter by revision
if (this.filters.revision !== null) {
tasks = _.filter(tasks, t => {
let payload = t.data.title + t.data.bugzilla_id + t.data.phid + t.data.diff_phid + t.data.id + t.data.diff_id
const payload = t.data.title + t.data.bugzilla_id + t.data.phid + t.data.diff_phid + t.data.id + t.data.diff_id
return payload.toLowerCase().indexOf(this.filters.revision.toLowerCase()) !== -1
})
}
@ -83,7 +83,7 @@ export default {
return this.$store.state.tasks ? this.$store.state.tasks.length : 0
},
states () {
let currentTasks = this.$store.state.tasks
const currentTasks = this.$store.state.tasks
const states = currentTasks.reduce((states, task) => {
if (states[task.data.state] === undefined) {
states[task.data.state] = 0
@ -94,12 +94,12 @@ export default {
// Order states by their nb, and calc percents
return Object.keys(states).map(state => {
let nb = states[state]
const nb = states[state]
return {
'key': state,
'name': state.startsWith('error.') ? 'error: ' + state.substring(6) : state,
'nb': nb,
'percent': currentTasks && currentTasks.length > 0 ? Math.round(nb * 100 / currentTasks.length) : 0
key: state,
name: state.startsWith('error.') ? 'error: ' + state.substring(6) : state,
nb,
percent: currentTasks && currentTasks.length > 0 ? Math.round(nb * 100 / currentTasks.length) : 0
}
}).sort((x, y) => { return y.nb - x.nb })
},

10
frontend/src/index.html Normal file
Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="<%= htmlWebpackPlugin.options.lang %>">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
</body>
</html>

Просмотреть файл

@ -3,14 +3,14 @@ export default {
methods: {
update_query (name, value) {
console.log('update query', name, value)
var query = Object.assign({}, this.$route.query)
const query = Object.assign({}, this.$route.query)
if (value !== null && value !== '') {
query[name] = value
} else if (name in query) {
delete query[name]
}
if (this.$router) {
this.$router.push({ 'query': query })
this.$router.push({ query })
}
}
}
@ -32,8 +32,8 @@ export default {
filters: {
// Display time since elapsed in a human format
since (datetime) {
var dspStep = (t, name) => {
let x = Math.round(t)
const dspStep = (t, name) => {
const x = Math.round(t)
if (x === 0) {
return ''
}
@ -41,15 +41,15 @@ export default {
}
let diff = (new Date() - new Date(datetime)) / 1000
let steps = [
const steps = [
[60, 'second'],
[60, 'minute'],
[24, 'hour'],
[30, 'day'],
[12, 'month']
]
var prev = ''
for (let [t, name] of steps) {
let prev = ''
for (const [t, name] of steps) {
if (diff > t) {
prev = dspStep(diff % t, name)
diff = diff / t

Просмотреть файл

@ -7,7 +7,7 @@ const TASKCLUSTER_DIFF_INDEX = 'https://index.taskcluster.net/v1/task/project.re
export default new Vuex.Store({
state: {
backend_url: process.env.BACKEND_URL,
backend_url: BACKEND_URL,
tasks: [],
diffs: {},
stats: null,
@ -33,7 +33,7 @@ export default new Vuex.Store({
},
use_tasks (state, tasks) {
state.tasks = state.tasks.concat(tasks['tasks'])
state.tasks = state.tasks.concat(tasks.tasks)
},
use_diffs (state, diffs) {
@ -47,7 +47,7 @@ export default new Vuex.Store({
},
use_revision (state, payload) {
state.revision = Object.assign(payload.revision, { 'diffs': payload.diffs })
state.revision = Object.assign(payload.revision, { diffs: payload.diffs })
},
// Store a new diff to display
@ -60,8 +60,8 @@ export default new Vuex.Store({
// Add issues to a diff
add_issues (state, payload) {
let update = {}
let issues = state.issues[payload.diffId] || []
const update = {}
const issues = state.issues[payload.diffId] || []
update[payload.diffId] = issues.concat(payload.issues)
state.issues = Object.assign({}, state.issues, update)
},
@ -86,24 +86,24 @@ export default new Vuex.Store({
// Load Phabricator diffs from our backend
// Load a single page at once, providing pagination state
load_diffs (state, payload) {
let url = payload.url || this.state.backend_url + '/v1/diff/'
const url = payload.url || BACKEND_URL + '/v1/diff/'
let params = payload.query || {}
return axios.get(url, { params: params }).then(resp => {
const params = payload.query || {}
return axios.get(url, { params }).then(resp => {
state.commit('use_diffs', resp.data)
})
},
// Load a specific diff and its issues
load_diff (state, diffId) {
let url = this.state.backend_url + '/v1/diff/' + diffId
const url = BACKEND_URL + '/v1/diff/' + diffId
state.commit('use_diff', null)
return axios.get(url).then(resp => {
state.commit('use_diff', resp.data)
// Load all issues in that diff
state.dispatch('load_issues', {
diffId: diffId,
diffId,
url: resp.data.issues_url
})
}).catch(err => {
@ -133,9 +133,9 @@ export default new Vuex.Store({
if (payload.url === undefined) {
state.commit('reset_stats')
}
const url = payload.url || this.state.backend_url + '/v1/check/stats/'
const url = payload.url || BACKEND_URL + '/v1/check/stats/'
let params = {}
const params = {}
if (payload.since !== undefined) {
params.since = payload.since
}
@ -157,8 +157,8 @@ export default new Vuex.Store({
// Store new issues for that check
load_check_issues (state, payload) {
const url = payload.url || this.state.backend_url + `/v1/check/${payload.repository}/${payload.analyzer}/${payload.check}/`
let params = {}
const url = payload.url || BACKEND_URL + `/v1/check/${payload.repository}/${payload.analyzer}/${payload.check}/`
const params = {}
if (payload.publishable !== undefined) {
params.publishable = payload.publishable
}
@ -171,7 +171,7 @@ export default new Vuex.Store({
},
load_repositories (state, payload) {
let url = this.state.backend_url + '/v1/repository/'
const url = BACKEND_URL + '/v1/repository/'
return axios.get(url).then(resp => {
// Assume we only have one page here
@ -182,15 +182,15 @@ export default new Vuex.Store({
// Retrieve diff data stored in Taskcluster index
// Do not persist that data in our store
load_taskcluster_diff (state, payload) {
let url = TASKCLUSTER_DIFF_INDEX + payload.id
const url = TASKCLUSTER_DIFF_INDEX + payload.id
return axios.get(url)
},
// Load a specific revision and its diffs
load_revision (state, payload) {
return Promise.all([
axios.get(this.state.backend_url + '/v1/revision/' + payload.id),
axios.get(this.state.backend_url + '/v1/revision/' + payload.id + '/diffs/')
axios.get(BACKEND_URL + '/v1/revision/' + payload.id),
axios.get(BACKEND_URL + '/v1/revision/' + payload.id + '/diffs/')
]).then(([respRevision, respDiffs]) => {
// Store revision & diffs data
state.commit('use_revision', {
@ -199,7 +199,7 @@ export default new Vuex.Store({
})
// Start loading issues for each diff
for (let diff of respDiffs.data.results) {
for (const diff of respDiffs.data.results) {
state.dispatch('load_issues', {
diffId: diff.id,
url: diff.issues_url
@ -210,7 +210,7 @@ export default new Vuex.Store({
// Load Phabricator indexed tasks summary from Taskcluster
load_index (state, payload) {
let channel = payload.channel || 'production'
const channel = payload.channel || 'production'
let url = `https://firefox-ci-tc.services.mozilla.com/api/index/v1/tasks/project.relman.${channel}.code-review.phabricator.diff?limit=200`
if (payload && payload.continuationToken) {
url += '&continuationToken=' + payload.continuationToken
@ -218,7 +218,7 @@ export default new Vuex.Store({
return axios.get(url).then(resp => {
state.commit('use_tasks', {
tasks: resp.data.tasks,
url: url
url
})
// Continue loading available tasks
@ -232,8 +232,8 @@ export default new Vuex.Store({
},
load_history (state, payload) {
let url = this.state.backend_url + '/v1/check/history/'
let params = payload || {}
const url = BACKEND_URL + '/v1/check/history/'
const params = payload || {}
// Reset
state.commit('use_history', [])

131
frontend/webpack.config.js Normal file
Просмотреть файл

@ -0,0 +1,131 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { merge } = require('webpack-merge')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const common = {
context: path.resolve(__dirname),
entry: ['./src/index.js'],
resolve: {
extensions: ['.js', '.vue']
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].bundle.js'
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
title: 'Mozilla Code Review Bot',
filename: 'index.html',
template: './src/index.html'
}),
new webpack.ProvidePlugin({
process: 'process/browser'
}),
// Define backend url as constant
// using an environment variable with fallback for devs
new webpack.DefinePlugin({
BACKEND_URL: JSON.stringify(process.env.BACKEND_URL || 'http://localhost:8000')
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
})
],
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 0
}
}
]
},
// Images: Copy image files to build folder
{ test: /\.(?:ico|gif|png|jpg|jpeg)$/i, type: 'asset/resource' },
// Fonts and SVGs: Inline files
{ test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, type: 'asset/inline' }
]
}
}
const development = {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
port: 8010,
hot: true,
historyApiFallback: true,
open: true
}
}
const production = {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
splitChunks: {
chunks: 'all',
maxInitialRequests: 5,
name: false
},
runtimeChunk: 'single'
},
performance: {
hints: 'error',
maxAssetSize: 1782579.2,
maxEntrypointSize: 2621440
},
plugins: [
new CleanWebpackPlugin({
verbose: false
})
]
}
module.exports = (env, args) => {
switch (args.mode) {
case 'development':
return merge(common, development)
case 'production':
return merge(common, production)
default:
throw new Error('No matching configuration was found!')
}
}