Add the skeleton of a new UI based on Polymer, PolyGerrit

This is the beginnings of an experimental new non-GWT web UI developed
using a modern JS web framework, http://www.polymer-project.org/. It
will coexist alongside the GWT UI until it is feature-complete.

The functionality of this change is light years from complete, with
a full laundry list of things that don't work. This change is simply
meant to get the starting work in and continue iteration afterward.

The contents of the polygerrit-ui directory started as the full tree of
https://github.com/andybons/polygerrit at 219f531, plus a few more
local changes since review started. In the future this directory will
be pruned, rearranged, and integrated with the Buck build.

Change-Id: Ifb6f5429e8031ee049225cdafa244ad1c21bf5b5
This commit is contained in:
Andrew Bonventre 2015-11-04 11:14:54 -05:00 committed by Andrew Bonventre
parent 1a468f047a
commit ba69835964
38 changed files with 2957 additions and 0 deletions

1
polygerrit-ui/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

5
polygerrit-ui/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
npm-debug.log
dist
bower_components
.tmp

11
polygerrit-ui/README.md Normal file
View File

@ -0,0 +1,11 @@
# PolyGerrit
For local testing against production data...
```sh
npm install
bower install
go run server.go
```
Then visit http://localhost:8081

View File

@ -0,0 +1,116 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./gr-change-list-view.html">
<link rel="import" href="./gr-change-view.html">
<link rel="import" href="./gr-diff-view.html">
<link rel="import" href="./gr-search-bar.html">
<dom-module id="gr-app">
<template>
<style>
:host {
display: flex;
min-height: 100vh;
flex-direction: column;
}
:host[constrained] main {
margin: 0 auto;
width: 100%;
max-width: 980px;
}
header,
footer {
background-color: #eee;
padding: 20px;
}
header {
display: flex;
align-items: center;
}
main {
flex: 1;
padding: 20px 0;
}
.bigTitle {
color: #000;
font-size: 24px;
text-decoration: none;
}
.bigTitle:hover {
text-decoration: underline;
}
.searchContainer {
display: flex;
flex: 1;
justify-content: flex-end;
}
gr-search-bar {
width: 500px;
}
</style>
<header role="banner">
<a href="/" class="bigTitle">PolyGerrit</a>
<div class="searchContainer">
<gr-search-bar value="[[params.query]]" role="search"></gr-search-bar>
</div>
</header>
<main>
<template is="dom-if" if="{{_showChangeList}}">
<gr-change-list-view params="[[params]]"></gr-change-list-view>
</template>
<template is="dom-if" if="{{_showChangeView}}" restamp="true">
<gr-change-view params="[[params]]"></gr-change-view>
</template>
<template is="dom-if" if="{{_showDiffView}}" restamp="true">
<gr-diff-view params="[[params]]"></gr-diff-view>
</template>
</main>
<footer role="contentinfo">Powered by PolyGerrit</footer>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-app',
properties: {
constrained: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
params: Object,
route: {
type: Object,
value: {},
observer: '_routeChanged',
}
},
_routeChanged: function(route) {
this.set('_showChangeList', route == 'gr-change-list');
this.set('_showChangeView', route == 'gr-change-view');
this.set('_showDiffView', route == 'gr-diff-view');
this.constrained = route == 'gr-change-view';
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,197 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./gr-date-formatter.html">
<dom-module id="gr-change-list-item">
<template>
<style>
:host {
display: table-row;
}
th, td {
border-bottom: 1px solid #eee;
padding: 2px 5px;
vertical-align: top;
}
th {
background: #eee;
text-align: left;
}
a {
color: #000;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.positionIndicator {
opacity: .1;
visibility: hidden;
}
.avatarImage {
border-radius: 50%;
height: 16px;
vertical-align: -3px;
width: 16px;
}
.u-monospace {
font-family: 'Source Code Pro';
}
.u-green {
color: #388E3C;
}
.u-red {
color: #D32F2F;
}
</style>
<template is="dom-if" if="[[header]]">
<th></th> <!-- keyboard position indicator -->
<th>Subject</th>
<th>Status</th>
<th>Owner</th>
<th>Project</th>
<th>Branch</th>
<th>Updated</th>
<th>Size</th>
<th title="Code-Review">CR</th>
<th title="Verified">V</th>
</template>
<template is="dom-if" if="[[!header]]">
<td>
<span class="positionIndicator">&#x25b6;</span>
</td>
<td>
<a href$="[[changeURL()]]">[[change.subject]]</a>
</td>
<td>[[_computeChangeStatusString(change)]]</td>
<td>
<img class="avatarImage" src$="[[_computeAvatarURL(change.owner)]]">
<a href$="[[_computeOwnerLink(change.owner.email)]]"
title$="[[_computeOwnerTitle(change.owner)]]">[[change.owner.name]]</a>
</td>
<td>
<a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
</td>
<td>
<a href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
</td>
<td><gr-date-formatter date-str="[[change.updated]]"></gr-date-formatter></td>
<td class="u-monospace">
<span class="u-green"><span>+</span>[[change.insertions]]</span>,
<span class="u-red"><span>-</span>[[change.deletions]]</span>
</td>
<td title="Code-Review"
class$="[[_computeCodeReviewClass(change.labels.Code-Review)]]">[[_computeCodeReviewLabel(change.labels.Code-Review)]]</td>
<td title="Verified" class="u-green">[[_computeVerifiedLabel(change.labels.Verified)]]</td>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-list-item',
properties: {
header: {
type: Boolean,
reflectToAttribute: true,
value: false,
},
selected: {
type: Boolean,
reflectToAttribute: true,
},
change: Object,
},
changeURL: function() {
if (!this.change) { return ''; }
return '/c/' + this.change._number + '/';
},
_computeChangeStatusString: function(change) {
if (!change.mergeable) {
return 'Merge Conflict';
}
return '';
},
_computeCodeReviewClass: function(codeReview) {
if (!codeReview) { return ''; }
if (codeReview.approved) {
return 'u-green';
}
if (codeReview.value == 1) {
return 'u-monospace u-green';
}
if (codeReview.value == -1) {
return 'u-monospace u-red';
}
return '';
},
_computeCodeReviewLabel: function(codeReview) {
if (!codeReview) { return ''; }
if (codeReview.approved) {
return '✓';
}
if (codeReview.value == 1) {
return '+1';
}
if (codeReview.value == -1) {
return '-1';
}
return '';
},
_computeVerifiedLabel: function(verified) {
if (verified && verified.approved) {
return '✓';
}
return ''
},
_computeAvatarURL: function(owner) {
if (!owner) { return ''; }
return '/accounts/' + owner.email + '/avatar?s=32'
},
_computeOwnerLink: function(email) {
if (!email) { return ''; }
return '/q/owner:' + encodeURIComponent(email) + '+status:open';
},
_computeOwnerTitle: function(owner) {
if (!owner) { return ''; }
// TODO: Is this safe from XSS attacks?
return owner.name + ' <' + owner.email + '>';
},
_computeProjectURL: function(project) {
return '/projects/' + project + ',dashboards/default';
},
_computeProjectBranchURL: function(project, branch) {
return '/q/status:open+project:' + project + '+branch:' + branch;
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,127 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="./gr-change-list.html">
<dom-module id="gr-change-list-view">
<template>
<style>
:host {
display: block;
}
gr-change-list {
margin: 0 20px;
width: calc(100% - 40px);
}
nav {
padding: 10px 0;
text-align: center;
}
nav a {
display: inline-block;
}
nav a:first-of-type {
margin-right: 10px;
}
[hidden] {
display: none !important;
}
</style>
<iron-ajax
auto
url="/changes/"
params="[[_computeQueryParams(query, offset)]]"
json-prefix=")]}'"
last-response="{{_changes}}"
debounce-duration="300"></iron-ajax>
<gr-change-list changes="{{_changes}}"></gr-change-list>
<nav>
<a href$="[[_computeNavLink(query, offset, -1)]]"
hidden$="[[_hidePrevArrow(offset)]]">&larr; Prev</a>
<a href$="[[_computeNavLink(query, offset, 1)]]"
hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
</nav>
</template>
<script>
(function() {
'use strict';
var DEFAULT_NUM_CHANGES = 25;
Polymer({
is: 'gr-change-list-view',
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
/**
* Change objects loaded from the server.
*/
_changes: Array,
},
_paramsChanged: function(value) {
this.query = value.query;
this.offset = value.offset || 0;
},
_computeQueryParams: function(query, offset) {
var options = Changes.listChangesOptionsToHex(
Changes.ListChangesOption.LABELS,
Changes.ListChangesOption.DETAILED_ACCOUNTS
);
var obj = {
n: DEFAULT_NUM_CHANGES, // Number of results to return.
O: options,
S: offset || 0,
};
if (query && query.length > 0) {
obj.q = query;
}
return obj;
},
_computeNavLink: function(query, offset, direction) {
// Offset could be a string when passed from the router.
offset = +(offset || 0);
var newOffset = Math.max(0, offset + (25 * direction));
var href = '/q/' + query;
if (newOffset > 0) {
href += ',' + newOffset;
}
return href;
},
_hidePrevArrow: function(offset) {
return offset == 0;
},
_hideNextArrow: function(changesLen) {
return changesLen < DEFAULT_NUM_CHANGES;
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,126 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-a11y-keys/iron-a11y-keys.html">
<link rel="import" href="./gr-change-list-item.html">
<dom-module id="gr-change-list">
<template>
<style>
:host {
display: table;
border: 1px solid #eee;
border-collapse: collapse;
}
:host:focus gr-change-list-item[selected]::shadow {
background-color: #d8EdF9;
}
:host gr-change-list-item[selected]::shadow .positionIndicator {
visibility: visible;
}
:host:focus gr-change-list-item[selected]::shadow .positionIndicator {
opacity: 1;
}
</style>
<iron-a11y-keys
target="[[keyTarget]]"
keys="j k enter"
on-keys-pressed="_handleKey"></iron-a11y-keys>
<gr-change-list-item header></gr-change-list-item>
<template is="dom-repeat" items="{{changes}}" as="change">
<gr-change-list-item change="[[change]]"
selected="[[_isSelected(index)]]"></gr-change-list-item>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-list',
hostAttributes: {
tabindex: 0,
},
properties: {
/**
* An array of ChangeInfo objects to render.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
*/
changes: Array,
keyTarget: {
type: Object,
value: function() {
return document.body;
}
},
selectedIndex: {
type: Number,
value: 0,
observer: '_selectedIndexChanged',
},
},
_isSelected: function(index) {
return index == this.selectedIndex;
},
_selectedIndexChanged: function(value) {
// Dont re-render the entire list.
var changeEls = this._getNonHeaderListItems();
for (var i = 0; i < changeEls.length; i++) {
changeEls[i].toggleAttribute('selected', i == value);
}
},
_handleKey: function(e) {
var len = (this.changes && this.changes.length) || 0;
switch(e.detail.combo) {
case 'j':
if (this.selectedIndex == len - 1) { return; }
this.selectedIndex += 1;
break;
case 'k':
if (this.selectedIndex == 0) { return; }
this.selectedIndex -= 1;
break;
case 'enter':
page(this._changeURLForIndex(this.selectedIndex));
break;
}
},
_changeURLForIndex: function(index) {
var changeEls = this._getNonHeaderListItems();
if (index < changeEls.length && changeEls[index]) {
return changeEls[index].changeURL();
}
return '';
},
_getNonHeaderListItems: function() {
return this.querySelectorAll('gr-change-list-item:not([header])');
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,195 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="./gr-date-formatter.html">
<link rel="import" href="./gr-file-list.html">
<link rel="import" href="./gr-messages-list.html">
<dom-module id="gr-change-view">
<template>
<style>
:host {
display: block;
}
.container {
margin: 0 20px;
}
.changeInfo,
.summary {
margin: 10px 0;
padding: 10px 0;
}
table {
border-collapse: collapse;
}
td {
padding: 2px 5px;
vertical-align: top;
}
.changeInfo-label {
font-weight: bold;
text-align: right;
}
.summary {
font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
margin: 10px 0;
overflow-x: auto;
padding: 10px 0;
white-space: pre-wrap;
}
.summary {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
</style>
<iron-ajax id="detailXHR"
url="[[_computeDetailPath(changeNum)]]"
params="[[_computeDetailQueryParams()]]"
json-prefix=")]}'"
last-response="{{change}}"
debounce-duration="300"></iron-ajax>
<iron-ajax id="commentsXHR"
url="[[_computeCommentsPath(changeNum)]]"
json-prefix=")]}'"
last-response="{{comments}}"
debounce-duration="300"></iron-ajax>
<div class="container">
<h2>[[change.subject]]</h2>
<div class="changeInfo">
<table>
<tr>
<td class="changeInfo-label">Owner</td>
<td>[[change.owner.name]]</td>
</tr>
<tr>
<td class="changeInfo-label">Reviewers</td>
<td>
<template is="dom-repeat"
items="[[_computeReviewers(change.labels.Code-Review.all, change.owner)]]"
as="reviewer">
<div>[[reviewer.name]]</div>
</template>
</td>
</tr>
<tr>
<td class="changeInfo-label">Project</td>
<td>[[change.project]]</td>
</tr>
<tr>
<td class="changeInfo-label">Branch</td>
<td>[[change.branch]]</td>
</tr>
<tr>
<td class="changeInfo-label">Topic</td>
<td>[[change.topic]]</td>
</tr>
<tr>
<td class="changeInfo-label">Strategy</td>
<td></td>
</tr>
<tr>
<td class="changeInfo-label">Updated</td>
<td>
<gr-date-formatter
date-str="[[change.updated]]"></gr-date-formatter>
</td>
</tr>
</table>
</div>
<div class="summary">[[_computeCurrentRevisionMessage(change)]]</div>
<gr-file-list change-num="[[changeNum]]"
patch-num="[[_computePatchNum(change.current_revision)]]"
revision="[[change.current_revision]]"
comments="[[comments]]"></gr-file-list>
<gr-messages-list change-num="[[changeNum]]"
messages="[[change.messages]]"
comments="[[comments]]"></gr-messages-list>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-change-view',
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
changeNum: Number,
},
_paramsChanged: function(value) {
this.changeNum = value.changeNum;
if (!this.changeNum) {
this.change = null;
this.comments = null;
return;
}
this.$.detailXHR.generateRequest();
this.$.commentsXHR.generateRequest();
},
_computeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeCommitInfoPath: function(changeNum, commitHash) {
return '/changes/' + changeNum + '/revisions/' + commitHash + '/commit';
},
_computeCommentsPath: function(changeNum) {
return '/changes/' + changeNum + '/comments';
},
_computePatchNum: function(revision) {
return this.change && this.change.revisions[revision]._number;
},
_computeDetailQueryParams: function() {
var options = Changes.listChangesOptionsToHex(
Changes.ListChangesOption.CURRENT_REVISION,
Changes.ListChangesOption.CURRENT_COMMIT,
Changes.ListChangesOption.CHANGE_ACTIONS
);
return { O: options };
},
_computeCurrentRevisionMessage: function(change) {
return change &&
change.revisions[change.current_revision].commit.message;
},
_computeReviewers: function(reviewers, owner) {
if (reviewers.length == 1) { return reviewers; }
return reviewers.filter(function(reviewer) {
return reviewer._account_id != owner._account_id;
});
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,90 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="gr-date-formatter">
<template>
<style>
:host {
display: inline;
}
</style>
<span>[[_computeDateStr(dateStr)]]</span>
</template>
<script>
(function() {
'use strict';
var Duration = {
HOUR: 1000 * 60 * 60,
DAY: 1000 * 60 * 60 * 24,
};
var ShortMonthNames = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
'Nov', 'Dec'
];
Polymer({
is: 'gr-date-formatter',
properties: {
dateStr: {
type: String,
value: null,
notify: true
}
},
_computeDateStr: function(dateStr) {
return this._dateStr(this._parseDateStr(dateStr), new Date());
},
_parseDateStr: function(dateStr) {
if (!dateStr) { return null; }
return util.parseDate(dateStr);
},
_dateStr: function(t, now) {
if (!t) { return ''; }
var diff = now.getTime() - t.getTime();
if (diff < Duration.DAY && t.getDay() == now.getDay()) {
// Within 24 hours and on the same day:
// '2:14 AM'
var pm = t.getHours() >= 12;
var hours = t.getHours() === 0 ? 12 :
pm ? t.getHours() - 12 : t.getHours();
var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
t.getMinutes();
return hours + ':' + minutes + (pm ? ' PM' : ' AM');
} else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
diff < 180 * Duration.DAY) {
// From one to six months:
// 'Aug 29'
return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
} else if (diff >= 180 * Duration.DAY) {
// More than six months:
// 'Aug 29, 1997'
return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
t.getFullYear();
}
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,273 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="gr-diff-view">
<template>
<style>
:host {
display: block;
}
.diffContainer {
display: flex;
font-family: 'Source Code Pro', monospace;
white-space: pre;
}
.diffNumbers {
border-right: 1px solid #eee;
color: #aaa;
padding: 0 10px;
text-align: right;
}
.diffContent {
padding-left: 2px;
}
.lineNum {
cursor: pointer;
}
.lineNum:hover {
text-decoration: underline;
}
.lightRed {
background-color: #fee;
}
.darkRed,
.delete span {
background-color: #faa;
}
.lightGreen {
background-color: #efe;
}
.darkGreen,
.insert span {
background-color: #9f9;
}
</style>
<iron-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
json-prefix=")]}'"
last-response="{{_change}}"
debounce-duration="300"></iron-ajax>
<iron-ajax
id="diffXHR"
url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]"
json-prefix=")]}'"
on-response="_handleDiffResponse"
debounce-duration="300"></iron-ajax>
<div class="diffContainer" id="diffContainer">
<div class="diffNumbers" id="leftDiffNumbers"></div>
<div class="diffContent" id="leftDiffContent"></div>
<div class="diffNumbers" id="rightDiffNumbers"></div>
<div class="diffContent" id="rightDiffContent"></div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff-view',
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
_change: Object,
_changeNum: String,
_basePatchNum: String,
_patchNum: String,
_path: String,
},
_paramsChanged: function(value) {
console.log(value)
this._changeNum = value.changeNum;
this._patchNum = value.patchNum;
this._basePatchNum = value.basePatchNum;
this._path = value.path;
console.log(this._basePatchNum);
if (!this._changeNum) {
this._change = null;
this._basePatchNum = null;
this._patchNum = null;
this._path = null;
return;
}
// Assign the params here since a computed binding relying on
// `_basePatchNum` wont fire in the case where its not defined.
this.$.diffXHR.params = this._diffQueryParams();
this.$.diffXHR.generateRequest();
},
_computeChangeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeChangeDetailQueryParams: function() {
var options = Changes.listChangesOptionsToHex(
Changes.ListChangesOption.ALL_REVISIONS
);
return { O: options };
},
_computeDiffPath: function(changeNum, patchNum, path) {
return '/changes/' + changeNum + '/revisions/' + patchNum + '/files/' +
encodeURIComponent(path) + '/diff';
},
_diffQueryParams: function(basePatchNum) {
var params = {
context: 'ALL',
intraline: null
};
if (!!basePatchNum) {
params.base = basePatchNum;
}
return params;
},
_handleDiffResponse: function(e, req) {
var diff = e.detail.response;
this._constructDOM(diff);
},
_constructDOM: function(diff) {
if (!diff.content) { return; }
var leftLineNum = 0 + (diff.content.skip || 0);
var rightLineNum = leftLineNum;
for (var i = 0; i < diff.content.length; i++) {
var diffChunk = diff.content[i];
if (diffChunk.ab) {
for (var j = 0; j < diffChunk.ab.length; j++) {
this._addRow(++leftLineNum, ++rightLineNum, diffChunk.ab[j],
diffChunk.ab[j]);
}
continue;
}
if (diffChunk.a || diffChunk.b) {
var aLen = (diffChunk.a && diffChunk.a.length) || 0;
var bLen = (diffChunk.b && diffChunk.b.length) || 0;
var maxLen = Math.max(aLen, bLen);
for (var j = 0; j < maxLen; j++) {
var leftContent;
if (diffChunk.a && j < diffChunk.a.length) {
leftContent = diffChunk.a[j];
leftLineNum++;
}
var rightContent;
if (diffChunk.b && j < diffChunk.b.length) {
rightContent = diffChunk.b[j];
rightLineNum++;
}
var leftHighlight;
if (diffChunk.edit_a && j < diffChunk.edit_a.length) {
leftHighlight = diffChunk.edit_a[j];
}
var rightHighlight;
if (diffChunk.edit_b && j < diffChunk.edit_b.length) {
rightHighlight = diffChunk.edit_b[j];
}
this._addRow(leftLineNum,
rightLineNum,
leftContent,
rightContent,
leftHighlight,
rightHighlight);
}
}
}
},
_addRow: function(leftLineNum,
rightLineNum,
leftContent,
rightContent,
leftHighlight,
rightHighlight) {
var leftLineNumEl = document.createElement('div');
var rightLineNumEl = document.createElement('div');
var leftColEl = document.createElement('div');
// These classes are added to account for Polymers polyfill behavior.
// In order to guarantee sufficient specificity within the CSS rules,
// these are added to every element. Since the Polymer DOM utility
// functions (which would do this automatically) are not being used for
// performance reasons, this is done manually.
leftColEl.className = 'style-scope gr-diff-view';
var rightColEl = document.createElement('div');
rightColEl.className = 'style-scope gr-diff-view';
leftLineNumEl.className = 'style-scope gr-diff-view lineNum';
rightLineNumEl.className = 'style-scope gr-diff-view lineNum';
// Ensure that all elements have content so they render at the correct
// height.
leftLineNumEl.textContent =
leftContent != undefined ? leftLineNum : '\n';
rightLineNumEl.textContent =
rightContent != undefined ? rightLineNum : '\n';
leftContent = leftContent || '\n';
rightContent = rightContent || '\n';
var leftHTML;
var rightHTML;
if (leftContent == rightContent) {
leftHTML = leftContent;
rightHTML = rightContent;
} else {
leftHTML = this._highlightedHTML(leftContent, leftHighlight);
rightHTML = this._highlightedHTML(rightContent, rightHighlight);
}
// If the html is just the text then it didn't get highlighted.
// Use textContent which is faster than innerHTML.
if (leftContent == leftHTML) {
leftColEl.textContent = leftContent;
} else {
leftColEl.innerHTML = leftHTML;
}
if (rightContent == rightHTML) {
rightColEl.textContent = rightContent;
} else {
rightColEl.innerHTML = rightHTML;
}
if (leftContent != rightContent) {
leftColEl.classList.add('delete');
rightColEl.classList.add('insert');
leftColEl.classList.add(leftHighlight ? 'lightRed' : 'darkRed');
rightColEl.classList.add(rightHighlight ? 'lightGreen' : 'darkGreen');
}
this.$.leftDiffNumbers.appendChild(leftLineNumEl);
this.$.leftDiffContent.appendChild(leftColEl);
this.$.rightDiffNumbers.appendChild(rightLineNumEl);
this.$.rightDiffContent.appendChild(rightColEl);
},
_highlightedHTML: function(content, range) {
return content;
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,129 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="gr-file-list">
<template>
<style>
:host {
display: block;
}
.tableContainer {
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
}
table a {
display: block;
}
td {
padding: 2px 0;
white-space: nowrap;
}
th {
text-align: left;
}
.status {
width: 20px;
}
</style>
<iron-ajax id="xhr"
url="[[_computeFilesURL(changeNum, revision)]]"
json-prefix=")]}'"
on-response="_handleResponse"
debounce-duration="300"></iron-ajax>
<div class="tableContainer">
<table>
<tr>
<th></th>
<th></th>
<th>Path</th>
<th>Stats</th>
</tr>
<template is="dom-repeat" items="{{files}}" as="file">
<tr>
<td></td>
<td class="status">[[file.status]]</td>
<td class="path">
<a class="file"
href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">[[file.__path]]</a>
</td>
<td>
+<span>[[file.lines_inserted]]</span> lines,
-<span>[[file.lines_deleted]]</span> lines
</td>
</tr>
</template>
</table>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-file-list',
properties: {
patchNum: Number,
changeNum: {
type: Number,
observer: '_changeNumOrRevisionChanged',
},
revision: {
type: String,
observer: '_changeNumOrRevisionChanged',
},
comments: {
type: Array,
value: [],
},
},
_changeNumOrRevisionChanged: function() {
if (!!this.changeNum && !!this.revision) {
this.$.xhr.generateRequest();
}
},
_computeFilesURL: function(changeNum, revision) {
return '/changes/' + changeNum + '/revisions/' + revision + '/files';
},
_handleResponse: function(e, req) {
var result = e.detail.response;
var paths = Object.keys(result).sort();
var files = [];
for (var i = 0; i < paths.length; i++) {
var info = result[paths[i]];
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
files.push(info)
}
this.files = files;
},
_computeDiffURL: function(changeNum, patchNum, path) {
return '/c/' + changeNum + '/' + patchNum + '/' + path;
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,217 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./gr-date-formatter.html">
<dom-module id="gr-message">
<template>
<style>
:host {
border-top: 1px solid #ddd;
display: block;
position: relative;
}
:host:not([expanded]) {
cursor: pointer;
}
.avatar {
border-radius: 50%;
}
.collapsed .contentContainer {
padding: 10px;
padding-right: 60px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
.expanded .contentContainer {
padding: 7px 0 10px;
}
.expanded .contentContainer {
margin-left: 45px;
}
.collapsed .contentContainer {
color: #777;
margin-left: 35px;
}
.avatar {
position: absolute;
left: 0;
}
.collapsed .avatar {
top: 8px;
}
.expanded .avatar {
top: 10px;
}
.collapsed .avatar {
height: 25px;
width: 25px;
}
.expanded .avatar {
height: 35px;
width: 35px;
}
.name {
font-weight: bold;
}
.collapsed .name,
.collapsed .content,
.collapsed .message {
display: inline;
}
.collapsed .comments {
display: none;
}
.collapsed .name,
.collapsed gr-date-formatter {
color: #000;
}
.expanded .name {
cursor: pointer;
}
.expanded .message,
.expanded .commentMessage {
white-space: pre-wrap;
}
gr-date-formatter {
position: absolute;
right: 0;
top: 10px;
}
.file {
border-top: 1px solid #ddd;
font-weight: bold;
margin: 10px 0 3px;
padding: 10px 0 5px;
}
.commentContainer {
display: flex;
margin: 5px 0;
}
.lineNum {
margin-right: 10px;
min-width: 75px;
}
.commentMessage {
flex: 1;
}
</style>
<div class$="[[_computeClass(expanded)]]">
<img class="avatar" src$="[[_computeAvatarURL(message.author)]]">
<div class="contentContainer">
<div class="name" id="name">[[message.author.name]]</div>
<div class="content">
<div class="message">[[message.message]]</div>
<div class="comments">
<template is="dom-repeat" items="{{files}}" as="file">
<div class="file">
<a href$="[[_computeFileDiffURL(file, changeNum, message._revision_number)]]">[[file]]</a>:
</div>
<template is="dom-repeat"
items="[[_computeCommentsForFile(file)]]" as="comment">
<div class="commentContainer">
<a class="lineNum"
href$="[[_computeDiffLineURL(file, changeNum, message._revision_number, comment)]]">
<template is="dom-if" if="[[comment.line]]">
Line <span>[[comment.line]]</span>:
</template>
<template is="dom-if" if="[[!comment.line]]">
File comment:
</template>
</a>
<div class="commentMessage">[[comment.message]]</div>
</div>
</template>
</template>
</div>
</div>
<gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
</div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-message',
listeners: {
'tap': '_tapHandler',
'name.tap': '_collapseHandler',
},
properties: {
changeNum: Number,
message: Object,
comments: {
type: Object,
observer: '_commentsChanged',
},
expanded: {
type: Boolean,
value: true,
reflectToAttribute: true,
},
},
_commentsChanged: function(value) {
this.files = Object.keys(value || {}).sort();
this.expanded = this.files.length > 0;
},
_computeFileDiffURL: function(file, changeNum, patchNum) {
return '/c/' + changeNum + '/' + patchNum + '/' + file;
},
_computeDiffLineURL: function(file, changeNum, patchNum, comment) {
var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
if (comment.line) {
diffURL += '#' + comment.line;
}
return diffURL;
},
_computeCommentsForFile: function(file) {
return this.comments[file];
},
_tapHandler: function(e) {
if (this.expanded) { return; }
this.expanded = true;
},
_collapseHandler: function(e) {
if (!this.expanded) { return; }
e.stopPropagation();
this.expanded = false;
},
_computeClass: function(expanded) {
return expanded ? 'expanded' : 'collapsed';
},
_computeAvatarURL: function(author) {
if (!author) { return '' }
return '/accounts/' + author.email + '/avatar?s=100';
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,81 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./gr-message.html">
<dom-module id="gr-messages-list">
<template>
<style>
:host {
display: block;
}
h3 {
margin: 20px 0 5px;
}
</style>
<h3>Messages</h3>
<template is="dom-repeat" items="{{messages}}" as="message">
<gr-message change-num="[[changeNum]]"
message="[[message]]"
comments="[[_computeCommentsForMessage(comments, message, index)]]"></gr-message>
</template>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-messages-list',
properties: {
changeNum: Number,
messages: {
type: Array,
value: [],
},
comments: Object,
},
_computeCommentsForMessage: function(comments, message, index) {
var comments = comments || {};
var messages = this.messages || [];
var msgComments = {};
var mDate = util.parseDate(message.date);
var nextMDate;
if (index < messages.length - 1) {
nextMDate = util.parseDate(messages[index + 1].date);
}
for (var file in comments) {
var fileComments = comments[file];
for (var i = 0; i < fileComments.length; i++) {
var cDate = util.parseDate(fileComments[i].updated);
if (cDate >= mDate) {
if (nextMDate && cDate >= nextMDate) {
continue;
}
msgComments[file] = msgComments[file] || [];
msgComments[file].push(fileComments[i]);
}
}
}
return msgComments;
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,92 @@
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-input/iron-input.html">
<dom-module id="gr-search-bar">
<template>
<style>
:host {
display: block;
}
form {
display: flex;
}
input,
button {
border: 1px solid #aaa;
font-family: inherit;
font-size: 14px;
padding: 2px 5px;
}
input {
flex: 1;
border-radius: 2px 0 0 2px;
}
button {
background-color: #f1f2f3;
border-radius: 0 2px 2px 0;
border-left-width: 0;
}
</style>
<form>
<input is="iron-input" id="searchInput" bind-value="{{inputVal}}">
<button type="submit" id="searchButton">Search</button>
</form>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-search-bar',
listeners: {
'value-changed': '_valueChangedHandler',
'searchInput.keydown': '_inputKeyDownHandler',
'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
},
properties: {
value: {
type: String,
value: '',
notify: true,
}
},
_valueChangedHandler: function(e) {
this.inputVal = e.detail.value;
},
_inputKeyDownHandler: function(e) {
if (e.keyCode == 13) {
// Enter was pressed.
this._preventDefaultAndNavigateToInputVal(e);
}
},
_preventDefaultAndNavigateToInputVal: function(e) {
e.preventDefault();
page.show('/q/' + this.inputVal);
},
});
})();
</script>
</dom-module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html lang="en">
<meta charset="utf-8">
<meta name="description" content="Gerrit Code Review">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PolyGerrit</title>
<!-- build:css /styles/main.css -->
<link rel="stylesheet" href="/styles/main.css">
<!-- endbuild-->
<!-- build:js /bower_components/webcomponentsjs/webcomponents-lite.min.js -->
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<!-- endbuild -->
<!-- Use Shadow DOM where supported.
https://www.polymer-project.org/1.0/docs/devguide/settings.html -->
<!--<script>
window.Polymer = window.Polymer || {};
window.Polymer.dom = 'shadow';
</script>-->
<!-- will be replaced with elements/gr-app.vulcanized.html -->
<link rel="import" href="/elements/gr-app.html">
<!-- endreplace-->
<body unresolved>
<template is="dom-bind" id="app">
<gr-app params="{{params}}" route="{{route}}"></gr-app>
</template>
<!-- build:js /scripts/app.js -->
<script src="/bower_components/page/page.js"></script>
<script src="/scripts/app.js"></script>
<script src="/scripts/changes.js"></script>
<script src="/scripts/util.js"></script>
<!-- endbuild-->

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@ -0,0 +1,66 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(document) {
'use strict';
// See https://github.com/Polymer/polymer/issues/1381
window.addEventListener('WebComponentsReady', function() {
// Middleware
function scrollToTop(ctx, next) {
document.body.scrollTop = 0;
next();
}
// Routes.
page('/', function() {
page.redirect('/q/status:open');
});
function queryHandler(data) {
app.route = 'gr-change-list';
app.params = data.params;
}
page('/q/:query,:offset', scrollToTop, queryHandler);
page('/q/:query', scrollToTop, queryHandler);
page(/^\/(\d+)\/?/, scrollToTop, function(ctx) {
page.redirect('/c/' + ctx.params[0]);
});
page('/c/:changeNum', scrollToTop, function(data) {
app.route = 'gr-change-view';
app.params = data.params;
});
page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, scrollToTop, function(ctx) {
app.route = 'gr-diff-view';
var params = {
changeNum: ctx.params[0],
basePatchNum: ctx.params[2],
patchNum: ctx.params[4],
path: ctx.params[5]
};
if (!params.patchNum) {
params.patchNum = params.basePatchNum;
delete(params.basePatchNum);
}
app.params = params;
});
page.start();
});
})(document);

View File

@ -0,0 +1,73 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
var Changes = Changes || {};
// Must be kept in sync with the ListChangesOption enum and protobuf.
Changes.ListChangesOption = {
LABELS: 0,
DETAILED_LABELS: 8,
// Return information on the current patch set of the change.
CURRENT_REVISION: 1,
ALL_REVISIONS: 2,
// If revisions are included, parse the commit object.
CURRENT_COMMIT: 3,
ALL_COMMITS: 4,
// If a patch set is included, include the files of the patch set.
CURRENT_FILES: 5,
ALL_FILES: 6,
// If accounts are included, include detailed account info.
DETAILED_ACCOUNTS: 7,
// Include messages associated with the change.
MESSAGES: 9,
// Include allowed actions client could perform.
CURRENT_ACTIONS: 10,
// Set the reviewed boolean for the caller.
REVIEWED: 11,
// Include download commands for the caller.
DOWNLOAD_COMMANDS: 13,
// Include patch set weblinks.
WEB_LINKS: 14,
// Include consistency check results.
CHECK: 15,
// Include allowed change actions client could perform.
CHANGE_ACTIONS: 16,
// Include a copy of commit messages including review footers.
COMMIT_FOOTERS: 17,
// Include push certificate information along with any patch sets.
PUSH_CERTIFICATES: 18
};
Changes.listChangesOptionsToHex = function() {
var v = 0;
for (var i = 0; i < arguments.length; i++) {
v |= 1 << arguments[i];
}
return v.toString(16);
};

View File

@ -0,0 +1,25 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
var util = util || {};
util.parseDate = function(dateStr) {
// Timestamps are given in UTC and have the format
// "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
// nanoseconds.
// Munge the date into an ISO 8061 format and parse that.
return new Date(dateStr.replace(' ', 'T') + 'Z');
};

View File

@ -0,0 +1,34 @@
/*
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
@import "//fonts.googleapis.com/css?family=Open+Sans:400,700";
@import "//fonts.googleapis.com/css?family=Source+Code+Pro";
*,
*::after,
*::before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
transition: none; /* Override the default Polymer fade-in. */
}
body {
font: 14px 'Open Sans', sans-serif;
}

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-app</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<!-- Step 1: import the element to test -->
<link rel="import" href="../elements/gr-app.html">
<test-fixture id="basic">
<template>
<gr-app></gr-app>
</template>
</test-fixture>
<script>
suite('gr-app tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-item</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-change-list-item.html">
<test-fixture id="basic">
<template>
<gr-change-list-item></gr-change-list-item>
</template>
</test-fixture>
<script>
suite('gr-change-list-item tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('computed fields', function() {
assert.equal(element._computeChangeStatusString({mergeable: true}), '');
assert.equal(element._computeChangeStatusString({mergeable: false}),
'Merge Conflict');
assert.equal(element._computeCodeReviewClass(), '');
assert.equal(element._computeCodeReviewClass({}), '');
assert.equal(element._computeCodeReviewClass({approved: true, value: 1}),
'u-green');
assert.equal(element._computeCodeReviewClass({value: 1}),
'u-monospace u-green');
assert.equal(element._computeCodeReviewClass({value: -1}),
'u-monospace u-red');
assert.equal(element._computeCodeReviewLabel(), '');
assert.equal(element._computeCodeReviewLabel({}), '');
assert.equal(element._computeCodeReviewLabel({approved: true, value: 1}),
'✓');
assert.equal(element._computeCodeReviewLabel({value: 1}), '+1');
assert.equal(element._computeCodeReviewLabel({value: -1}), '-1');
assert.equal(element._computeVerifiedLabel(), '');
assert.equal(element._computeVerifiedLabel({}), '');
assert.equal(element._computeVerifiedLabel({approved: true}), '✓');
assert.equal(element._computeOwnerLink('andybons+gerrit@gmail.com'),
'/q/owner:andybons%2Bgerrit%40gmail.com+status:open');
assert.equal(element._computeOwnerTitle(
{
name: 'Andrew Bonventre',
email: 'andybons+gerrit@gmail.com'
}),
'Andrew Bonventre <andybons+gerrit@gmail.com>');
// TODO(andybons): _computeProjectURL once it's not a constant.
assert.equal(element._computeProjectBranchURL(
'combustible-stuff', 'lemons'),
'/q/status:open+project:combustible-stuff+branch:lemons');
element.change = { _number: 42 };
assert.equal(element.changeURL(), '/c/42/');
});
});
</script>

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-change-list.html">
<test-fixture id="basic">
<template>
<gr-change-list></gr-change-list>
</template>
</test-fixture>
<script>
suite('gr-change-list tests', function() {
var changeList;
setup(function() {
changeList = fixture('basic');
});
test('keyboard shortcuts', function() {
changeList.changes = [
{_number: 0},
{_number: 1},
{_number: 2},
];
flushAsynchronousOperations();
var changeListItems =
changeList.querySelectorAll('gr-change-list-item:not([header])');
assert.equal(changeListItems.length, 3);
assert.equal(changeList.selectedIndex, 0);
MockInteractions.pressAndReleaseKeyOn(changeList, 74); // 'j'
flushAsynchronousOperations();
assert.equal(changeList.selectedIndex, 1);
MockInteractions.pressAndReleaseKeyOn(changeList, 74); // 'j'
flushAsynchronousOperations();
assert.equal(changeList.selectedIndex, 2);
MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
flushAsynchronousOperations();
assert.equal(changeList.selectedIndex, 1);
MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
flushAsynchronousOperations();
MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
flushAsynchronousOperations();
MockInteractions.pressAndReleaseKeyOn(changeList, 75); // 'k'
flushAsynchronousOperations();
assert.equal(changeList.selectedIndex, 0);
});
});
</script>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-list-view</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<!-- Step 1: import the element to test -->
<link rel="import" href="../elements/gr-change-list-view.html">
<test-fixture id="basic">
<template>
<gr-change-list-view></gr-change-list-view>
</template>
</test-fixture>
<script>
suite('gr-change-list-view tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-view</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<script src="../scripts/changes.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<!-- Step 1: import the element to test -->
<link rel="import" href="../elements/gr-change-view.html">
<test-fixture id="basic">
<template>
<gr-change-view></gr-change-view>
</template>
</test-fixture>
<script>
suite('gr-change-view tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-date-formatter</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<script src="../scripts/util.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-date-formatter.html">
<test-fixture id="basic">
<template>
<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
</template>
</test-fixture>
<script>
suite('gr-date-formatter tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('date is parsed correctly', function() {
assert.notOk((new Date('foo')).valueOf());
var d = element._parseDateStr(element.getAttribute('date-str'));
assert.isAbove(d.valueOf(), 0);
});
function normalizedDate(dateStr) {
var d = new Date(dateStr);
d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
return d;
}
function testDates(nowStr, dateStr, expected) {
var now = normalizedDate(nowStr);
var t = normalizedDate(dateStr);
assert.equal(element._dateStr(t, now), expected);
}
test('dates strings are correct', function() {
// Within 24 hours on same day.
testDates('2015-07-29T20:34:00.000Z',
'2015-07-29T15:34:00.000Z',
'3:34 PM');
// Within 24 hours on different days.
testDates('2015-07-29T03:34:00.000Z',
'2015-07-28T20:25:00.000Z',
'Jul 28');
// More than 24 hours. Less than six months.
testDates('2015-07-29T20:34:00.000Z',
'2015-06-15T03:25:00.000Z',
'Jun 15');
// More than six months.
testDates('2015-09-15T20:34:00.000Z',
'2015-01-15T03:25:00.000Z',
'Jan 15, 2015');
});
});
</script>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-diff-view.html">
<test-fixture id="basic">
<template>
<gr-diff></gr-diff>
</template>
</test-fixture>
<script>
suite('gr-diff-view tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-file-list.html">
<test-fixture id="basic">
<template>
<gr-file-list></gr-file-list>
</template>
</test-fixture>
<script>
suite('gr-file-list tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-message</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<!-- Step 1: import the element to test -->
<link rel="import" href="../elements/gr-message.html">
<test-fixture id="basic">
<template>
<gr-message></gr-message>
</template>
</test-fixture>
<script>
suite('gr-message tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-messages-list</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<!-- Step 1: import the element to test -->
<link rel="import" href="../elements/gr-messages-list.html">
<test-fixture id="basic">
<template>
<gr-messages-list></gr-messages-list>
</template>
</test-fixture>
<script>
suite('gr-messages-list tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-search-bar</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/test-fixture/test-fixture-mocha.js"></script>
<link rel="import" href="../../bower_components/test-fixture/test-fixture.html">
<link rel="import" href="../elements/gr-search-bar.html">
<test-fixture id="basic">
<template>
<search-bar></search-bar>
</template>
</test-fixture>
<script>
suite('gr-search-bar tests', function() {
var element;
setup(function() {
element = fixture('basic');
});
test('', function() {
});
});
</script>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<!--
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>Elements Test Runner</title>
<meta charset="utf-8">
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script>
WCT.loadSuites([
'gr-change-list-test.html',
'gr-search-bar-test.html',
'gr-date-formatter-test.html',
'gr-diff-view-test.html',
'gr-app-test.html',
'gr-change-view-test.html',
'gr-change-list-item-test.html',
'gr-change-list-view-test.html',
'gr-file-list-test.html',
'gr-messages-list-test.html',
'gr-message-test.html']);
</script>

21
polygerrit-ui/bower.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "polygerrit",
"version": "0.0.0",
"authors": [
"Andrew Bonventre <andybons@google.com>"
],
"description": "Gerrit UI in Polymer",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^1.1",
"page": "visionmedia/page.js#~1.6",
"iron-ajax": "PolymerElements/iron-ajax#~1.0",
"iron-a11y-keys": "PolymerElements/iron-a11y-keys#~1.0",
"iron-input": "PolymerElements/iron-input#~1.0",
"iron-test-helpers": "PolymerElements/iron-test-helpers#~1.0"
},
"devDependencies": {
"web-component-tester": "*",
"test-fixture": "PolymerElements/test-fixture#^1.0.0"
}
}

174
polygerrit-ui/gulpfile.js Normal file
View File

@ -0,0 +1,174 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
// Include Gulp & tools we'll use
var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var del = require('del');
var runSequence = require('run-sequence');
var merge = require('merge-stream');
var path = require('path');
var historyApiFallback = require('connect-history-api-fallback');
var AUTOPREFIXER_BROWSERS = [
'ie >= 10',
'ie_mob >= 10',
'ff >= 30',
'chrome >= 34',
'safari >= 7',
'opera >= 23',
'ios >= 7',
'android >= 4.4',
'bb >= 10'
];
var styleTask = function (stylesPath, srcs) {
return gulp.src(srcs.map(function(src) {
return path.join('app', stylesPath, src);
}))
.pipe($.changed(stylesPath, {extension: '.css'}))
.pipe($.autoprefixer(AUTOPREFIXER_BROWSERS))
.pipe(gulp.dest('.tmp/' + stylesPath))
.pipe($.cssmin())
.pipe(gulp.dest('dist/' + stylesPath))
.pipe($.size({title: stylesPath}));
};
var imageOptimizeTask = function (src, dest) {
return gulp.src(src)
.pipe($.cache($.imagemin({
progressive: true,
interlaced: true
})))
.pipe(gulp.dest(dest))
.pipe($.size({title: 'images'}));
};
var optimizeHtmlTask = function (src, dest) {
var assets = $.useref.assets({searchPath: ['.tmp', 'app', 'dist']});
return gulp.src(src)
// Replace path for vulcanized assets
.pipe($.if('*.html', $.replace('elements/gr-app.html', 'elements/gr-app.vulcanized.html')))
.pipe(assets)
// Concatenate and minify JavaScript
.pipe($.if('*.js', $.uglify({preserveComments: 'some'})))
// Concatenate and minify styles
// In case you are still using useref build blocks
.pipe($.if('*.css', $.cssmin()))
.pipe(assets.restore())
.pipe($.useref())
// Minify any HTML
.pipe($.if('*.html', $.minifyHtml({
quotes: true,
empty: true,
spare: true
})))
// Output files
.pipe(gulp.dest(dest))
.pipe($.size({title: 'html'}));
};
// Compile and automatically prefix stylesheets
gulp.task('styles', function () {
return styleTask('styles', ['**/*.css']);
});
gulp.task('elements', function () {
return styleTask('elements', ['**/*.css']);
});
// Optimize images
gulp.task('images', function () {
return imageOptimizeTask('app/images/**/*', 'dist/images');
});
// Copy all files at the root level (app)
gulp.task('copy', function () {
var app = gulp.src([
'app/*',
'!app/test'
], {
dot: true
}).pipe(gulp.dest('dist'));
var bower = gulp.src([
'bower_components/**/*'
]).pipe(gulp.dest('dist/bower_components'));
var elements = gulp.src(['app/elements/**/*.html',
'app/elements/**/*.css',
'app/elements/**/*.js'])
.pipe(gulp.dest('dist/elements'));
var vulcanized = gulp.src(['app/elements/gr-app.html'])
.pipe($.rename('gr-app.vulcanized.html'))
.pipe(gulp.dest('dist/elements'));
return merge(app, bower, elements, vulcanized)
.pipe($.size({title: 'copy'}));
});
// Copy web fonts to dist
gulp.task('fonts', function () {
return gulp.src(['app/fonts/**'])
.pipe(gulp.dest('dist/fonts'))
.pipe($.size({title: 'fonts'}));
});
// Scan your HTML for assets & optimize them
gulp.task('html', function () {
return optimizeHtmlTask(
['app/**/*.html', '!app/{elements,test}/**/*.html'],
'dist');
});
// Vulcanize granular configuration.
gulp.task('vulcanize', function () {
var DEST_DIR = 'dist/elements';
return gulp.src('dist/elements/gr-app.vulcanized.html')
.pipe($.vulcanize({
stripComments: true,
inlineCss: true,
inlineScripts: true
}))
.pipe(gulp.dest(DEST_DIR))
.pipe($.size({title: 'vulcanize'}));
});
// Clean output directory
gulp.task('clean', function (cb) {
del(['.tmp', 'dist'], cb);
});
// Build production files, the default task
gulp.task('default', ['clean'], function (cb) {
// Uncomment 'cache-config' if you are going to use service workers.
runSequence(
['copy', 'styles'],
'elements',
['images', 'fonts', 'html'],
'vulcanize', // 'cache-config',
cb);
});
// Load tasks for web-component-tester
// Adds tasks for `gulp test:local` and `gulp test:remote`
require('web-component-tester').gulp.init(gulp);
// Load custom tasks from the `tasks` directory
try { require('require-dir')('tasks'); } catch (err) {}

View File

@ -0,0 +1,39 @@
{
"private": true,
"devDependencies": {
"browser-sync": "^2.7.7",
"connect-history-api-fallback": "^1.1.0",
"del": "^1.1.1",
"gulp": "^3.8.5",
"gulp-autoprefixer": "^2.1.0",
"gulp-cache": "^0.2.8",
"gulp-changed": "^1.0.0",
"gulp-cssmin": "^0.1.7",
"gulp-flatten": "0.0.4",
"gulp-if": "^1.2.1",
"gulp-imagemin": "^2.2.1",
"gulp-jshint": "^1.6.3",
"gulp-load-plugins": "^0.10.0",
"gulp-minify-html": "^1.0.2",
"gulp-rename": "^1.2.0",
"gulp-replace": "^0.5.3",
"gulp-size": "^1.0.0",
"gulp-uglify": "^1.2.0",
"gulp-useref": "^1.1.2",
"gulp-vulcanize": "^6.0.0",
"jshint-stylish": "^2.0.0",
"merge-stream": "^0.1.7",
"opn": "^1.0.0",
"require-dir": "^0.3.0",
"run-sequence": "^1.0.2",
"vulcanize": ">= 1.4.2",
"web-component-tester": "^3.1.3"
},
"scripts": {
"test": "gulp test:local",
"start": "gulp serve"
},
"engines": {
"node": ">=0.10.0"
}
}

128
polygerrit-ui/server.go Normal file
View File

@ -0,0 +1,128 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bufio"
"compress/gzip"
"errors"
"flag"
"io"
"log"
"net"
"net/http"
"net/url"
"regexp"
"strings"
)
var (
restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
port = flag.String("port", ":8081", "Port to serve HTTP requests on")
prod = flag.Bool("prod", false, "Serve production assets")
)
func main() {
flag.Parse()
if *prod {
http.Handle("/", http.FileServer(http.Dir("dist")))
} else {
http.Handle("/bower_components/",
http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components"))))
http.Handle("/", http.FileServer(http.Dir("app")))
}
http.HandleFunc("/changes/", handleRESTProxy)
http.HandleFunc("/accounts/", handleRESTProxy)
log.Println("Serving on port", *port)
log.Fatal(http.ListenAndServe(*port, &server{}))
}
func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req := &http.Request{
Method: "GET",
URL: &url.URL{
Scheme: "https",
Host: *restHost,
Opaque: r.URL.EscapedPath(),
RawQuery: r.URL.RawQuery,
},
}
res, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer res.Body.Close()
if _, err := io.Copy(w, res.Body); err != nil {
log.Println("Error copying response to ResponseWriter:", err)
return
}
}
type gzipResponseWriter struct {
io.WriteCloser
http.ResponseWriter
}
func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter {
gz := gzip.NewWriter(w)
return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w}
}
func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.WriteCloser.Write(b)
}
func (w gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("gzipResponseWriter: ResponseWriter does not satisfy http.Hijacker interface")
}
return h.Hijack()
}
type server struct{}
// Any path prefixes that should resolve to index.html.
var (
fePaths = []string{"/q/", "/c/"}
issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
)
func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL)
for _, prefix := range fePaths {
if strings.HasPrefix(r.URL.Path, prefix) {
r.URL.Path = "/"
log.Println("Redirecting to /")
break
} else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil {
r.URL.Path = "/"
log.Println("Redirecting to /")
break
}
}
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
http.DefaultServeMux.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gzw := newGzipResponseWriter(w)
defer gzw.Close()
http.DefaultServeMux.ServeHTTP(gzw, r)
}

17
polygerrit-ui/wct.conf.js Normal file
View File

@ -0,0 +1,17 @@
var path = require('path');
var ret = {
suites: ['app/test'],
webserver: {
pathMappings: []
}
};
var mapping = {};
var rootPath = (__dirname).split(path.sep).slice(-1)[0];
mapping['/components/' + rootPath + '/app/bower_components'] = 'bower_components';
ret.webserver.pathMappings.push(mapping);
module.exports = ret;