зеркало из https://github.com/mozilla/treeherder.git
change the layout of the bottom panel
This commit is contained in:
Родитель
a0d4386031
Коммит
1f19d91fdf
|
@ -124,13 +124,43 @@ body {
|
|||
}
|
||||
|
||||
.job-btn {
|
||||
margin: 0 -4px 0 0;
|
||||
margin: 0 -3px 0 0;
|
||||
padding: 0 2px 0 2px;
|
||||
}
|
||||
|
||||
.th-notes-accordion .accordion-toggle {
|
||||
padding: 0;
|
||||
}
|
||||
div.navbar-fixed-bottom{
|
||||
border-top: 1px solid #ccc;
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.table-super-condensed thead > tr > th,
|
||||
.table-super-condensed tbody > tr > th,
|
||||
.table-super-condensed tfoot > tr > th,
|
||||
.table-super-condensed thead > tr > td,
|
||||
.table-super-condensed tbody > tr > td,
|
||||
.table-super-condensed tfoot > tr > td {
|
||||
padding: 2px;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
div.navbar-fixed-bottom .panel{
|
||||
box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 0px 2px 0px rgba(0, 0, 0, 0.8) inset
|
||||
}
|
||||
|
||||
div.navbar-fixed-bottom dt,
|
||||
div.navbar-fixed-bottom dt {
|
||||
font-size:.8em;
|
||||
}
|
||||
|
||||
div.navbar-fixed-bottom .close-btn{
|
||||
position:absolute;
|
||||
top:-15px;
|
||||
right:5px;
|
||||
z-index:10;
|
||||
}
|
||||
|
||||
/* Custom Job buttons*/
|
||||
.btn-orange {
|
||||
|
|
|
@ -63,16 +63,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="nav navbar navbar-default navbar-fixed-bottom"
|
||||
ng-show="selectedJob">
|
||||
<div class="container">
|
||||
<button type="button"
|
||||
class="close pull-right"
|
||||
aria-hidden="true"
|
||||
ng-click="clearJob()">×</button>
|
||||
<div class="nav navbar navbar-default navbar-fixed-bottom" ng-show="selectedJob">
|
||||
<button type="button" class="btn btn-xs close-btn btn-danger pull-right" ng-click="clearJob()">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</button>
|
||||
<div ng-include src="'plugins/pluginpanel.html'"></div>
|
||||
|
||||
|
||||
<div ng-include src="'plugins/pluginpanel.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="vendor/angular/angular.js"></script>
|
||||
|
@ -93,8 +90,9 @@
|
|||
<script src="js/controllers/machines.js"></script>
|
||||
<script src="js/controllers/timeline.js"></script>
|
||||
<script src="plugins/controller.js"></script>
|
||||
<script src="plugins/jobdetail/controller.js"></script>
|
||||
<script src="plugins/jobfoo/controller.js"></script>
|
||||
<script src="plugins/tinderbox/controller.js"></script>
|
||||
<script src="plugins/tinderbox/directives.js"></script>
|
||||
<script src="plugins/bugs/controller.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
<script src="https://tbpl.mozilla.org/js/Config.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -82,7 +82,6 @@ treeherder.directive('thStar', function ($parse, thStarTypes) {
|
|||
});
|
||||
|
||||
treeherder.directive('thShowJobs', function ($parse, thResultStatusInfo) {
|
||||
|
||||
return {
|
||||
link: function(scope, element, attrs) {
|
||||
scope.$watch('resultSeverity', function(newVal) {
|
||||
|
|
|
@ -100,3 +100,24 @@ treeherder.factory('thRepos',
|
|||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
treeherder.factory('thJobNote', function($resource, $http, thUrl) {
|
||||
var JobNote = $resource(thUrl.getProjectUrl("/note/"));
|
||||
|
||||
// Workaround to the fact that $resource strips trailing slashes
|
||||
// out of urls. This causes a 301 redirect on POST because it does a
|
||||
// preflight OPTIONS call. Tastypie gives up on the POST after this
|
||||
// and nothing happens. So this alternative "thSave" command avoids
|
||||
// that by using the trailing slash directly in a POST call.
|
||||
// @@@ This may be fixed in later versions of Angular. Or perhaps there's
|
||||
// a better way?
|
||||
JobNote.prototype.thSave = function() {
|
||||
$http.post(thUrl.getProjectUrl("/note/"), {
|
||||
job_id: this.job_id,
|
||||
note: this.note,
|
||||
who: this.who,
|
||||
failure_classification_id: this.failure_classification_id
|
||||
});
|
||||
};
|
||||
return JobNote;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
"use strict";
|
||||
|
||||
treeherder.controller('BugsPluginCtrl',
|
||||
function BugsPluginCtrl($scope, $rootScope, $log) {
|
||||
$log.log("bugs plugin initialized");
|
||||
$scope.$watch('jobArtifacts', function(newValue, oldValue){
|
||||
$scope.open_bugs = []
|
||||
$scope.closed_bugs = []
|
||||
$log.log(newValue)
|
||||
if (newValue && newValue.hasOwnProperty('Open bugs')){
|
||||
$scope.open_bugs = newValue['Open bugs'].blob;
|
||||
}
|
||||
if (newValue && newValue.hasOwnProperty('Closed bugs')){
|
||||
$scope.closed_bugs = newValue['Closed bugs'].blob;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
<div ng-controller="BugsPluginCtrl">
|
||||
open bugs:
|
||||
<ul >
|
||||
<li ng-repeat="bug in open_bugs">
|
||||
{{bug}}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
closed bugs:
|
||||
<ul >
|
||||
<li ng-repeat="bug in closed_bugs">
|
||||
{{bug}}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
|
@ -1,19 +1,107 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
treeherder.controller('PluginCtrl',
|
||||
function PluginCtrl($scope, $rootScope) {
|
||||
function PluginCtrl($scope, $rootScope, $resource, $http,
|
||||
thServiceDomain, thUrl, thJobNote, thStarTypes, $log) {
|
||||
|
||||
$scope.$watch('selectedJob', function(newValue, oldValue) {
|
||||
// preferred way to get access to the selected job
|
||||
if (newValue) {
|
||||
$scope.job = newValue;
|
||||
$scope.artifacts = {}
|
||||
|
||||
var undef = "---undefined---";
|
||||
// fields that will show in the job detail panel
|
||||
$scope.visibleFields = {
|
||||
"Result": $scope.job.result || undef,
|
||||
"Job GUID": $scope.job.job_guid || undef,
|
||||
"Machine Platform Arch": $scope.job.machine_platform_architecture || undef,
|
||||
"Machine Platform OS": $scope.job.machine_platform_os || undef,
|
||||
"Build Platform": $scope.job.build_platform || undef,
|
||||
"Build Arch": $scope.job.build_architecture || undef,
|
||||
"Build OS": $scope.job.build_os || undef
|
||||
};
|
||||
/*this call retrieves (again) a job detail. can we avoid it?*/
|
||||
$http.get(thServiceDomain + $scope.job.resource_uri).
|
||||
success(function(data) {
|
||||
$scope.logs = data.logs;
|
||||
|
||||
data.artifacts.forEach(function(artifact) {
|
||||
if (artifact.name != "Structured Log") {
|
||||
// we don't return the blobs with job, just
|
||||
// resource_uris to them. For the Job Artifact,
|
||||
// we want that blob, so we need to fetch the
|
||||
// detail to get the blob which has the
|
||||
// tinderbox_printlines, etc.
|
||||
$scope.artifacts[artifact.name] =$resource(
|
||||
thServiceDomain + artifact.resource_uri).get();
|
||||
} else {
|
||||
// for the structured log, we don't need the blob
|
||||
// here, we have everything we need in the artifact
|
||||
// as is, so just save it.
|
||||
/*are we using this artifact here?*/
|
||||
$scope.lvArtifact=artifact;
|
||||
$scope.lvUrl = thUrl.getLogViewerUrl(artifact.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
$scope.updateNotes();
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.starTypes = thStarTypes;
|
||||
var JobNote = thJobNote;
|
||||
|
||||
// load the list of existing notes (including possibly a new one just
|
||||
// added).
|
||||
$scope.updateNotes = function() {
|
||||
$scope.notes = JobNote.query({job_id: $scope.job.job_id});
|
||||
};
|
||||
// when notes comes in, then set the latest note for the job
|
||||
$scope.$watch('notes', function(newValue, oldValue) {
|
||||
if (newValue && newValue.length > 0) {
|
||||
$scope.job.note=newValue[0];
|
||||
}
|
||||
});
|
||||
|
||||
// open form to create a new note
|
||||
$scope.addNote = function() {
|
||||
var fci = 0;
|
||||
if ($scope.notes && $scope.notes.length > 0) {
|
||||
fci = $scope.notes[0].failure_classification_id;
|
||||
}
|
||||
$scope.newNote = new JobNote({
|
||||
job_id: $scope.job.job_id,
|
||||
note: "",
|
||||
who: $scope.username,
|
||||
failure_classification_id: fci
|
||||
});
|
||||
$scope.focusInput=true;
|
||||
};
|
||||
|
||||
// done adding a new note, so clear and hide the form
|
||||
$scope.clearNewNote = function() {
|
||||
$scope.newNote = null;
|
||||
};
|
||||
|
||||
// save the note and hide the form
|
||||
$scope.saveNote = function() {
|
||||
$scope.newNote.thSave();
|
||||
$scope.updateNotes();
|
||||
$scope.clearNewNote();
|
||||
};
|
||||
|
||||
$scope.tabs = [
|
||||
{
|
||||
title: "Jobs Detail",
|
||||
content: "plugins/jobdetail/main.html",
|
||||
active: true
|
||||
title: "Tinderbox",
|
||||
content: "plugins/tinderbox/main.html"
|
||||
},
|
||||
{
|
||||
title: "Jobs Foo",
|
||||
content: "plugins/jobfoo/main.html"
|
||||
title: "Bug suggestions",
|
||||
content: "plugins/bugs/main.html"
|
||||
}
|
||||
];
|
||||
|
||||
}
|
||||
);
|
||||
);
|
|
@ -1,112 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
treeherder.controller('JobDetailPluginCtrl',
|
||||
function JobDetailPluginCtrl($scope, $resource, $http,
|
||||
thServiceDomain, thUrl, thJobNote, thStarTypes) {
|
||||
|
||||
$scope.$watch('selectedJob', function(newValue, oldValue) {
|
||||
// preferred way to get access to the selected job
|
||||
if (newValue) {
|
||||
$scope.job = newValue;
|
||||
|
||||
var undef = "---undefined---";
|
||||
// fields that will show in the job detail panel
|
||||
$scope.visibleFields = {
|
||||
"Result": $scope.job.result || undef,
|
||||
"Job GUID": $scope.job.job_guid || undef,
|
||||
"Machine Platform Arch": $scope.job.machine_platform_architecture || undef,
|
||||
"Machine Platform OS": $scope.job.machine_platform_os || undef,
|
||||
"Build Platform": $scope.job.build_platform || undef,
|
||||
"Build Arch": $scope.job.build_architecture || undef,
|
||||
"Build OS": $scope.job.build_os || undef
|
||||
};
|
||||
$http.get(thServiceDomain + $scope.job.resource_uri).
|
||||
success(function(data) {
|
||||
$scope.logs = data.logs;
|
||||
|
||||
data.artifacts.forEach(function(artifact) {
|
||||
if (artifact.name.indexOf("Job Artifact") !== -1) {
|
||||
// we don't return the blobs with job, just
|
||||
// resource_uris to them. For the Job Artifact,
|
||||
// we want that blob, so we need to fetch the
|
||||
// detail to get the blob which has the
|
||||
// tinderbox_printlines, etc.
|
||||
$scope.jobArtifact = $resource(
|
||||
thServiceDomain + artifact.resource_uri).get();
|
||||
} else if (artifact.name === "Structured Log") {
|
||||
// for the structured log, we don't need the blob
|
||||
// here, we have everything we need in the artifact
|
||||
// as is, so just save it.
|
||||
$scope.lvArtifact=artifact;
|
||||
$scope.lvUrl = thUrl.getLogViewerUrl(artifact.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
$scope.updateNotes();
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.starTypes = thStarTypes;
|
||||
var JobNote = thJobNote;
|
||||
|
||||
// load the list of existing notes (including possibly a new one just
|
||||
// added).
|
||||
$scope.updateNotes = function() {
|
||||
$scope.notes = JobNote.query({job_id: $scope.job.job_id});
|
||||
};
|
||||
// when notes comes in, then set the latest note for the job
|
||||
$scope.$watch('notes', function(newValue, oldValue) {
|
||||
if (newValue && newValue.length > 0) {
|
||||
$scope.job.note=newValue[0];
|
||||
}
|
||||
});
|
||||
|
||||
// open form to create a new note
|
||||
$scope.addNote = function() {
|
||||
var fci = 0;
|
||||
if ($scope.notes && $scope.notes.length > 0) {
|
||||
fci = $scope.notes[0].failure_classification_id;
|
||||
}
|
||||
$scope.newNote = new JobNote({
|
||||
job_id: $scope.job.job_id,
|
||||
note: "",
|
||||
who: $scope.username,
|
||||
failure_classification_id: fci
|
||||
});
|
||||
$scope.focusInput=true;
|
||||
};
|
||||
|
||||
// done adding a new note, so clear and hide the form
|
||||
$scope.clearNewNote = function() {
|
||||
$scope.newNote = null;
|
||||
};
|
||||
|
||||
// save the note and hide the form
|
||||
$scope.saveNote = function() {
|
||||
$scope.newNote.thSave();
|
||||
$scope.updateNotes();
|
||||
$scope.clearNewNote();
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
treeherder.factory('thJobNote', function($resource, $http, thUrl) {
|
||||
var JobNote = $resource(thUrl.getProjectUrl("/note/"));
|
||||
|
||||
// Workaround to the fact that $resource strips trailing slashes
|
||||
// out of urls. This causes a 301 redirect on POST because it does a
|
||||
// preflight OPTIONS call. Tastypie gives up on the POST after this
|
||||
// and nothing happens. So this alternative "thSave" command avoids
|
||||
// that by using the trailing slash directly in a POST call.
|
||||
// @@@ This may be fixed in later versions of Angular. Or perhaps there's
|
||||
// a better way?
|
||||
JobNote.prototype.thSave = function() {
|
||||
$http.post(thUrl.getProjectUrl("/note/"), {
|
||||
job_id: this.job_id,
|
||||
note: this.note,
|
||||
who: this.who,
|
||||
failure_classification_id: this.failure_classification_id
|
||||
});
|
||||
};
|
||||
return JobNote;
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
<div ng-controller="JobDetailPluginCtrl">
|
||||
<div class="span12">
|
||||
<div class="span7">
|
||||
<button class="btn btn-default btn-xs"
|
||||
title="add note"
|
||||
ng-click="addNote()">
|
||||
<i class="glyphicon glyphicon-comment"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default btn-xs disabled">Log:</button>
|
||||
<div class="btn btn-default btn-xs"
|
||||
ng-disabled="!lvArtifact">
|
||||
<a target="_blank" href="{{ lvUrl }}">Structured</a>
|
||||
</div>
|
||||
<div class="btn btn-default btn-xs"
|
||||
ng-repeat="joblog in logs">
|
||||
<a target="_blank" href="{{ joblog.url }}">Raw</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt class="label label-info">Machine name</dt>
|
||||
<dd><a href="https://secure.pub.build.mozilla.org/builddata/reports/slave_health/slave.html?name={{ $scope.job.machine_name }}">{{ job.machine_name }}</a></dd>
|
||||
<span ng-repeat="(label, value) in visibleFields">
|
||||
<dt class="label label-info" ng-bind="label"> </dt>
|
||||
<dd ng-bind-html="value" target="_blank"> </dd>
|
||||
</span>
|
||||
</dl>
|
||||
</div>
|
||||
<ul class="span7" >
|
||||
<li ng-repeat="line in jobArtifact.blob.tinderbox_printlines">
|
||||
<div ng-bind-html="line"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="span12">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>
|
||||
<i class="glyphicon glyphicon-plus" ng-click="addNote()" title="add note"></i>
|
||||
<span class="label label-info">Notes</span>
|
||||
</dt>
|
||||
<dd class="th-notes-accordion">
|
||||
<div ng-hide="notes.length==0">
|
||||
<span th-star star-id="notes[0].failure_classification_id"></span> {{job.note.failure_classification_id}}{{ notes[0].note_timestamp*1000|date:'medium' }} <span class="label label-info" ng-bind="notes[0].who"></span> said <em>"<span ng-bind="notes[0].note"></span>"</em>
|
||||
</div>
|
||||
<span ng-show="notes.length==0">No notes</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<div ng-show="newNote">
|
||||
<form ng-submit="saveNote()" class="form-inline">
|
||||
<select ng-model="newNote.failure_classification_id">
|
||||
<option ng-repeat="(value, star) in starTypes" value="{{value}}">{{star.name}}</option>
|
||||
</select>
|
||||
<input ng-model="newNote.note" focus-me="focusInput"/>
|
||||
<input class="btn btn-primary" type="submit" value="save">
|
||||
<a class="btn btn-default" ng-click="clearNewNote()">cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
treeherder.controller('JobFooPluginCtrl',
|
||||
function JobFooPluginCtrl($scope) {
|
||||
|
||||
$scope.$watch('selectedJob', function(newValue, oldValue) {
|
||||
// preferred way to get access to the selected job
|
||||
if (newValue) {
|
||||
$scope.job = newValue;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
);
|
|
@ -1,3 +0,0 @@
|
|||
<div ng-controller="JobFooPluginCtrl">
|
||||
<p>I pitty the foo that don't like job_guid: {{ job.job_guid }}</p>
|
||||
</div>
|
|
@ -1,7 +1,69 @@
|
|||
<div ng-controller="PluginCtrl">
|
||||
<tabset class="tabs-below">
|
||||
<tab ng-repeat="tab in tabs" heading="{{ tab.title }}" active="tab.active" disabled="tab.disabled">
|
||||
<ng-include src="tab.content"></ng-include>
|
||||
</tab>
|
||||
</tabset>
|
||||
<!--first panel begin-->
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-lg-5">
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<button class="btn btn-default btn-xs"
|
||||
title="add note"
|
||||
ng-click="addNote()">
|
||||
<i class="glyphicon glyphicon-comment"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default btn-xs disabled">Log:</button>
|
||||
<div class="btn btn-default btn-xs"
|
||||
ng-disabled="!lvArtifact">
|
||||
<a target="_blank" href="{{ lvUrl }}">Structured</a>
|
||||
</div>
|
||||
<div class="btn btn-default btn-xs"
|
||||
ng-repeat="joblog in logs">
|
||||
<a target="_blank" href="{{ joblog.url }}">Raw</a>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Machine name</dt>
|
||||
<dd><a href="https://secure.pub.build.mozilla.org/builddata/reports/slave_health/slave.html?name={{ $scope.job.machine_name }}">{{ job.machine_name }}</a></dd>
|
||||
<span ng-repeat="(label, value) in visibleFields">
|
||||
<dt>{{ label }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
</span>
|
||||
</dl>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>
|
||||
<i class="glyphicon glyphicon-plus" ng-click="addNote()" title="add note"></i>
|
||||
<span class="label label-info">Notes</span>
|
||||
</dt>
|
||||
<dd class="th-notes-accordion">
|
||||
<div ng-hide="notes.length==0">
|
||||
<span th-star star-id="notes[0].failure_classification_id"></span> {{job.note.failure_classification_id}}{{ notes[0].note_timestamp*1000|date:'medium' }} <span class="label label-info" ng-bind="notes[0].who"></span> said <em>"<span ng-bind="notes[0].note"></span>"</em>
|
||||
</div>
|
||||
<span ng-show="notes.length==0">No notes</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<div ng-show="newNote">
|
||||
<form ng-submit="saveNote()" class="form-inline">
|
||||
<select ng-model="newNote.failure_classification_id">
|
||||
<option ng-repeat="(value, star) in starTypes" value="{{value}}">{{star.name}}</option>
|
||||
</select>
|
||||
<input ng-model="newNote.note" focus-me="focusInput"/>
|
||||
<input class="btn btn-primary" type="submit" value="save">
|
||||
<a class="btn btn-default" ng-click="clearNewNote()">cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--first panel end-->
|
||||
<div class="col-sm-12 col-lg-7">
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<tabset class="tabs-below">
|
||||
<tab ng-repeat="tab in tabs" heading="{{ tab.title }}" active="tab.active" disabled="tab.disabled">
|
||||
<ng-include src="tab.content"></ng-include>
|
||||
</tab>
|
||||
</tabset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
"use strict";
|
||||
|
||||
treeherder.controller('TinderboxPluginCtrl',
|
||||
function TinderboxPluginCtrl($scope, $rootScope, $log) {
|
||||
$log.log("Tinderbox plugin initialized");
|
||||
$scope.$watch('artifacts', function(newValue, oldValue){
|
||||
$scope.tinderbox_lines = []
|
||||
$log.log(newValue)
|
||||
if (newValue && newValue.hasOwnProperty('Job Info')){
|
||||
$scope.tinderbox_lines = newValue['Job Info'].blob.tinderbox_printlines;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
treeherder.directive('thPrintLine', function() {
|
||||
console.log("thPrintline initialized")
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
line: '=line'
|
||||
},
|
||||
template: '<span>{{line}}</span>'
|
||||
};
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
<div ng-controller="TinderboxPluginCtrl">
|
||||
<ul >
|
||||
<li ng-repeat="line in tinderbox_lines">
|
||||
{{line}}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
Загрузка…
Ссылка в новой задаче