Updating the dashboard graphs to 3.0

This includes styling the graphs, moving the filters around, and changing the tabs to take up less space. Also made the graphs responsive.
The footer background color also changes to match the main body background.
This commit is contained in:
Jared Tabor 2015-11-17 14:21:25 -08:00
parent 21b6529101
commit 48b52a6681
17 changed files with 371 additions and 140 deletions

View File

@ -1093,13 +1093,13 @@ input[type="checkbox"].checkbox-no-label {
.icon-job-changed:before,
.icon-job-ok:before,
.icon-job-OK:before,
.icon-job-failed:before,
.icon-job-skipped:before {
content: "\f111";
}
.icon-job-stopped:before,
.icon-job-error:before,
.icon-job-failed:before,
.icon-job-canceled:before,
.icon-job-unreachable:before {
content: "\f06a";

View File

@ -20,10 +20,6 @@ body {
background-color: #f6f6f6;
}
.container-fluid {
padding-left: 20px;
padding-right: 20px;
}
#main-menu-container {
.navbar {
margin-bottom: 0;

View File

@ -35,11 +35,12 @@ svg.nvd3-svg {
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1.0);
background-color: #848992;
padding: 1px;
border: 1px solid rgba(0,0,0,.2);
border: 1px solid #848992;
border-radius: 5px;
z-index: 10000;
color: #ffffff;
font-family: Arial;
font-size: 13px;
text-align: left;
@ -70,7 +71,7 @@ svg.nvd3-svg {
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
padding: 8px;
padding: 10px;
}
.nvtooltip h3 {
@ -127,9 +128,11 @@ svg.nvd3-svg {
}
.nvtooltip table td.legend-color-guide div {
width: 8px;
height: 8px;
width: 12px;
height: 12px;
vertical-align: middle;
border: 1px solid #ffffff;
border-radius: 5px;
}
.nvtooltip .footer {
@ -569,6 +572,8 @@ svg.nvd3-svg {
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
font-weight: bold;
fill: #848992;
font-family: 'Open Sans';
}
.nvd3.nv-historicalStockChart .nv-dragTarget {

View File

@ -476,7 +476,7 @@ nv.nearestValueIndex = function (values, searchVal, threshold) {
theadEnter.append("tr")
.append("td")
.attr("colspan",3)
.append("strong")
// .append("strong")
.classed("x-value",true)
.html(headerFormatter(d.value));
@ -9744,8 +9744,8 @@ nv.models.scatterChart = function() {
, showYAxis = true
, rightAlignYAxis = false
, tooltips = true
, tooltipX = function(key, x, y) { return '<strong>' + x + '</strong>' }
, tooltipY = function(key, x, y) { return '<strong>' + y + '</strong>' }
, tooltipX = function(key, x, y) { return '<div>' + x + '</div>' }
, tooltipY = function(key, x, y) { return '<div>' + y + '</div>' }
, tooltip = function(key, x, y, date) { return '<h3>' + key + '</h3>'
+ '<p>' + date + '</p>' }
, state = nv.utils.state()

View File

@ -3,54 +3,76 @@
@import "../../shared/branding/colors.less";
.DashboardGraphs {
margin-top: 15px;
border: solid 1px #a9a9a9;
border-radius: 4px;
margin-top: 20px;
border: solid 1px #e1e1e1;
border-radius: 5px;
background-color: #ffffff;
padding-top:20px;
padding-left: 20px;
padding-right: 20px;
}
.DashboardGraphs-tabSection {
flex: 1;
.DashboardGraphs-headerSection{
display: flex;
// align-items: baseline;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: flex-start;
}
.DashboardGraphs-tab {
flex: 1;
padding: 10px;
border-right: solid 1px @disabled-item-border;
border-bottom: solid 1px @disabled-item-border;
color: #b7b7b7;
background-color: #ffffff;
font-size: 12px;
border: 1px solid #e1e1e1;
height: 30px;
border-radius: 5px;
margin-right: 20px;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
padding-top: 5px;
transition: background-color 0.2s;
text-transform: uppercase;
text-align: center;
font-size: 20px;
color: @disabled-item-text;
background-color: @disabled-item-background;
white-space: nowrap;
}
.DashboardGraphs-tab--firstTab {
border-top-left-radius: 4px;
width: 90px;
}
.DashboardGraphs-tab--lastTab {
border-top-right-radius: 4px;
border-right: 0;
width:100px;
margin-right: auto;
}
.DashboardGraphs-tab:hover {
color: #000;
color: #b7b7b7;
background-color: #f6f6f6;
cursor: pointer;
}
.DashboardGraphs-tab.is-selected {
background-color: @enabled-item-background;
color: @enabled-item-text;
border-bottom: 0;
.DashboardGraphs-tab:active {
color: #b7b7b7;
background-color: #d7d7d7;
cursor: pointer;
}
.DashboardGraphs-tab.is-selected:hover {
cursor: default;
.DashboardGraphs-tab:focus {
color: #b7b7b7;
}
.DashboardGraphs-tab.is-selected {
color: #ffffff;
background-color: #d7d7d7;
}
.DashboardGraphs-graphSection {
display: block;
flex: 1;
padding-top:20px;
}
.DashboardGraphs-graphContainer {
@ -63,25 +85,72 @@
display: block;
}
.DashboardGraphs-filterLabelIcon{
color: #d7d7d7;
font-size: 14px;
padding-top: 5px;
}
.DashboardGraphs-filterLabel{
color: #d7d7d7;
font-size: 12px;
padding-right: 10px;
padding-left: 10px;
padding-top: 2px;
text-transform: uppercase;
padding-top:5px;
}
.DashboardGraphs-graph {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
margin-bottom: 19px;
padding: 20px;
}
.nv-axislabel {
font-weight: bold !important;
fill: #b7b7b7 !important;
font-family: 'Open Sans' !important;
}
.nv-axis text {
fill: #b7b7b7 !important; //rgb(169, 178, 189);
font-family: 'Open Sans' !important;
}
.DashboardGraphs-graphToolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 6px;
flex-direction: row;
flex-wrap: wrap;
}
.DashboardGraphs-filterDropdown {
.DashboardGraphs-filterDropdownText {
flex: initial;
color: #b7b7b7;
background-color: #ffffff;
font-size: 12px;
padding-right: 15px;
text-transform: uppercase;
white-space: nowrap;
padding-right: 10px;
padding-left: 10px;
height: 20px;
border: 1px solid #e1e1e1;
border-radius: 5px;
transition: background-color 0.2s;
}
.DashboardGraphs-filterDropdownText:hover {
color: #b7b7b7;
background-color: #f6f6f6;
}
.DashboardGraphs-filterIcon{
color: #d7d7d7;
font-size: 14px;
width: 20px;
padding-left:10px;
padding-right: 10px;
}
.DashboardGraphs-filterDropdownItems {
@ -89,6 +158,15 @@
left: auto;
top: auto;
box-shadow: none;
text-transform: uppercase;
}
.DashboardGraphs-periodDropdown{
padding-top:5px;
}
.DashboardGraphs-jobTypeDropdown{
padding-top:5px;
}
.DashboardGraphs-filterDropdownItems--period {
@ -98,3 +176,41 @@
.DashboardGraphs-filterDropdownItems--jobType {
margin-left: -84px;
}
.DashboardGraphs-statusFilters{
padding-top: 5px;
}
.DashboardGraphs-statusFilter{
color: #b7b7b7;
background-color: #ffffff;
font-size: 12px;
text-transform: uppercase;
padding-right: 10px;
padding-left: 10px;
height: 20px;
border: 1px solid #e1e1e1;
border-radius: 5px;
transition: background-color 0.2s;
margin-left: 10px;
line-height:1;
}
.DashboardGraphs-statusFilter:hover{
cursor: pointer;
background-color: #f6f6f6;
}
.DashboardGraphs-statusFilter.is-selected {
color: #ffffff;
background-color: #d7d7d7;
}
.DashboardGraphs-hostStatusLabel--successful{
text-anchor: start !important;
}
.DashboardGraphs-hostStatusLabel--failed{
text-anchor: end !important;
}

View File

@ -12,6 +12,11 @@ export default
scope.hostStatusSelected = false;
}
function clearStatus() {
scope.isSuccessful = true;
scope.isFailed = true;
}
scope.toggleGraphStatus = function (graphType) {
clearGraphs();
if (graphType === "jobStatus") {
@ -22,7 +27,38 @@ export default
scope.$broadcast("resizeGraphs");
};
scope.toggleJobStatusGraph = function (status) {
if (status === "successful") {
scope.isSuccessful = !scope.isSuccessful;
if(!scope.isSuccessful && scope.isFailed){
status = 'successful';
}
else if(scope.isSuccessful && scope.isFailed){
status = 'both';
}
else if(!scope.isSuccessful && !scope.isFailed){
status = 'successful';
scope.isFailed = true;
}
} else if (status === "failed") {
scope.isFailed = !scope.isFailed;
if(scope.isSuccessful && scope.isFailed){
status = 'both';
}
if(scope.isSuccessful && !scope.isFailed){
status = 'failed';
}
else if(!scope.isSuccessful && !scope.isFailed){
status = 'failed';
scope.isSuccessful = true;
}
}
scope.$broadcast("jobStatusChange", status);
};
// initially toggle jobStatus graph
clearStatus();
clearGraphs();
scope.toggleGraphStatus("jobStatus");
}

View File

@ -1,5 +1,5 @@
<div class="DashboardGraphs">
<div class="DashboardGraphs-tabSection">
<div class="DashboardGraphs-headerSection">
<div class="DashboardGraphs-tab DashboardGraphs-tab--firstTab"
ng-click="toggleGraphStatus('jobStatus')"
ng-class="{'is-selected': jobStatusSelected }">
@ -10,6 +10,70 @@
ng-class="{'is-selected': hostStatusSelected }">
Host Status
</div>
<div class="DashboardGraphs-graphToolbar" ng-show="!hostStatusSelected">
<i class="fa fa-filter DashboardGraphs-filterLabelIcon"></i>
<div class="DashboardGraphs-filterLabel">Period</div>
<div class="DashboardGraphs-periodDropdown">
<a id="period-dropdown" role="button"
data-toggle="dropdown"
data-target="#"
href="/page.html"
class="DashboardGraphs-filterDropdownText">
Past Month <i class="fa fa-chevron-down DashboardGraphs-filterIcon"></i>
</a>
<ul class="dropdown-menu DashboardGraphs-filterDropdownItems
DashboardGraphs-filterDropdownItems--period" role="menu" aria-labelledby="period-dropdown">
<li>
<a class="n" id="day" >Past 24 Hours </a>
</li>
<li>
<a class="n" id="week">Past Week</a>
</li>
<li>
<a class="n" id="month">Past Month</a>
</li>
</ul>
</div>
<div class="DashboardGraphs-filterLabel">Job Type</div>
<div class="DashboardGraphs-jobTypeDropdown">
<a id="type-dropdown" role="button" data-toggle="dropdown" data-target="#" class="DashboardGraphs-filterDropdownText"
href="/page.html">
All <i class="fa fa-chevron-down DashboardGraphs-filterIcon"></i>
</a>
<ul class="dropdown-menu DashboardGraphs-filterDropdownItems
DashboardGraphs-filterDropdownItems--jobType" role="menu" aria-labelledby="type-dropdown">
<li>
<a class="m" id="all">All</a>
</li>
<li>
<a class="m" id="inv_sync">Inventory Sync</a>
</li>
<li>
<a class="m" id="scm_update">SCM Update</a>
</li>
<li>
<a class="m" id="playbook_run">Playbook Run</a>
</li>
</ul>
</div>
<div class="DashboardGraphs-statusFilters">
<button class="DashboardGraphs-statusFilter DashboardGraphs-statusFilter--jobStatus"
ng-click="toggleJobStatusGraph('successful')"
ng-class="{'is-selected': isSuccessful }">
<i class="fa icon-job-successful DashboardGraphs-statusFilterIcon">
</i> Successful
</button>
<button class="DashboardGraphs-statusFilter DashboardGraphs-statusFilter--jobStatus"
ng-click="toggleJobStatusGraph('failed')"
ng-class="{'is-selected': isFailed }">
<i class="fa icon-job-failed DashboardGraphs-statusFilterIcon">
</i> Failed
</button>
</div>
</div>
</div>
<div class="DashboardGraphs-graphSection">
<div class="DashboardGraphs-graphContainer" auto-size-module

View File

@ -20,7 +20,7 @@ function AutoSizeModule($window) {
function adjustSize() {
if (attrs.graphType === "hostStatus") {
if (element.parent().width() > 596) {
element.height(596);
element.height(320);//596);
} else {
element.height(element.parent().width());
}

View File

@ -24,7 +24,8 @@ function HostStatusGraph($compile, $window, adjustGraphSize, templateUrl) {
scope.$watch(attr.data, function(data) {
if (data && data.hosts) {
createGraph(data);
scope.data = data;
createGraph();
}
});
@ -52,31 +53,52 @@ function HostStatusGraph($compile, $window, adjustGraphSize, templateUrl) {
$(".DashboardGraphs-graph--hostStatusGraph").removeResize(adjustHostGraphSize);
});
function createGraph(data) {
if(data.hosts.total+data.hosts.failed>0){
data = [
{ "label": "Successful",
"color": "#60D66F",
"value" : data.hosts.total - data.hosts.failed
} ,
{ "label": "Failed",
function createGraph() {
var data, colors, color;
if(scope.data.hosts.total+scope.data.hosts.failed>0){
if(scope.status === "successful"){
data = [
{ "label": "SUCCESSFUL",
"color": "#5bbdbf",
"value" : scope.data.hosts.total - scope.data.hosts.failed
}];
colors = ['#5bbdbf'];
}
else if (scope.status === "failed"){
data = [{ "label": "FAILED",
"color" : "#ff5850",
"value" : data.hosts.failed
}
];
"value" : scope.data.hosts.failed
}];
colors = ['#ff5850'];
}
else {
data = [
{ "label": "SUCCESSFUL",
"color": "#5bbdbf",
"value" : scope.data.hosts.total - scope.data.hosts.failed
} ,
{ "label": "FAILED",
"color" : "#ff5850",
"value" : scope.data.hosts.failed
}
];
colors = ['#5bbdbf', '#ff5850'];
}
host_pie_chart = nv.models.pieChart()
.margin({bottom: 15})
.x(function(d) { return d.label; })
.x(function(d) {
return d.label +': '+ Math.round((d.value/scope.data.hosts.total)*100) + "%";
})
.y(function(d) { return d.value; })
.showLabels(true)
.showLegend(false)
.growOnHover(false)
.labelThreshold(0.01)
.tooltipContent(function(x, y) {
return '<b>'+x+'</b>'+ '<p>' + Math.floor(y.replace(',','')) + ' Hosts ' + '</p>';
return '<p>'+x+'</p>'+ '<p>' + Math.floor(y.replace(',','')) + ' HOSTS ' + '</p>';
})
.labelType("percent")
.color(['#60D66F', '#ff5850']);
.color(colors);
d3.select(element.find('svg')[0])
.datum(data)
@ -88,6 +110,33 @@ function HostStatusGraph($compile, $window, adjustGraphSize, templateUrl) {
"font-weight":400,
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
if(scope.status === "failed"){
color = "#ff5850";
}
else{
color = "#5bbdbf";
}
d3.select(element.find(".nv-label text")[0])
.attr("class", "DashboardGraphs-hostStatusLabel--successful")
.style({
"font-family": 'Open Sans',
"text-anchor": "start",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : color,
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
d3.select(element.find(".nv-label text")[1])
.attr("class", "DashboardGraphs-hostStatusLabel--failed")
.style({
"font-family": 'Open Sans',
"text-anchor" : "end !imporant",
"font-size": "16px",
"text-transform" : "uppercase",
"fill" : "#ff5850",
"src": "url(/static/assets/OpenSans-Regular.ttf)"
});
adjustGraphSize();
return host_pie_chart;

View File

@ -1 +1 @@
<svg width="100%" height="100%" preserveAspectRatio="xMinYMin"></svg>
<svg width="100%" height="100%" ></svg>

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 40 B

View File

@ -34,35 +34,46 @@ function JobStatusGraph($rootScope, $compile , $location, $window, Wait, adjustG
scope.$watch('data', function(value) {
if (value) {
createGraph(scope.period, scope.jobType, value);
createGraph(scope.period, scope.jobType, value, scope.status);
}
}, true);
function recreateGraph(period, jobType) {
graphDataService.get(period, jobType)
function recreateGraph(period, jobType, status) {
graphDataService.get(period, jobType, status)
.then(function(data) {
scope.data = data;
scope.period = period;
scope.jobType = jobType;
scope.status = status;
});
}
function createGraph(period, jobtype, data){
scope.$on('jobStatusChange', function(event, status){
recreateGraph(scope.period, scope.jobType, status);
});
function createGraph(period, jobtype, data, status){
scope.period = period;
scope.jobType = jobtype;
scope.status = status;
var timeFormat, graphData = [
{ "color": "#60D66F",
"key": "Successful",
{ "color": "#5bbdbf",
"key": "SUCCESSFUL",
"values": data.jobs.successful
},
{ "key" : "Failed" ,
{ "key" : "FAILED" ,
"color" : "#ff5850",
"values": data.jobs.failed
}
];
graphData = _.reject(graphData, function(num){
if(status!== undefined && status === num.key.toLowerCase()){
return num;
}
});
if(period==="day") {
timeFormat="%H:%M";
}
@ -82,20 +93,22 @@ function JobStatusGraph($rootScope, $compile , $location, $window, Wait, adjustG
job_status_chart
.x(function(d,i) { return i; })
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.showLegend(true) //Show the legend, allowing users to turn on/off line series.
.showLegend(false) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true); //Show the x-axis
job_status_chart.interactiveLayer.tooltip.fixedTop(-10); //distance from the top of the chart to tooltip
job_status_chart.interactiveLayer.tooltip.distance(-1); //distance from interactive line to tooltip
job_status_chart.xAxis
.axisLabel("Time")//.showMaxMin(true)
.axisLabel("TIME")//.showMaxMin(true)
.tickFormat(function(d) {
var dx = graphData[0].values[d] && graphData[0].values[d].x || 0;
return dx ? d3.time.format(timeFormat)(new Date(Number(dx+'000'))) : '';
});
job_status_chart.yAxis //Chart y-axis settings
.axisLabel('Jobs')
.axisLabel('JOBS')
.tickFormat(d3.format('.f'));
d3.select(element.find('svg')[0])
@ -109,27 +122,25 @@ function JobStatusGraph($rootScope, $compile , $location, $window, Wait, adjustG
});
// when the Period drop down filter is used, create a new graph based on the
d3.selectAll(element.find(".n"))
.on("click", function() {
$('.n').on("click", function(){
period = this.getAttribute("id");
$('#period-dropdown').replaceWith("<a id=\"period-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
$('#period-dropdown').replaceWith("<a id=\"period-dropdown\" class=\"DashboardGraphs-filterDropdownText\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+
"<i class=\"fa fa-chevron-down DashboardGraphs-filterIcon\"></i>\n");
scope.$parent.isFailed = true;
scope.$parent.isSuccessful = true;
recreateGraph(period, job_type);
});
//On click, update with new data
d3.selectAll(element.find(".m"))
.on("click", function() {
$('.m').on("click", function(){
job_type = this.getAttribute("id");
$('#type-dropdown').replaceWith("<a id=\"type-dropdown\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+"<span class=\"caret\"><span>\n");
$('#type-dropdown').replaceWith("<a id=\"type-dropdown\" class=\"DashboardGraphs-filterDropdownText\" role=\"button\" data-toggle=\"dropdown\" data-target=\"#\" href=\"/page.html\">"+this.text+
"<i class=\"fa fa-chevron-down DashboardGraphs-filterIcon\"></i>\n");
scope.$parent.isFailed = true;
scope.$parent.isSuccessful = true;
recreateGraph(period, job_type);
});
job_status_chart.legend.margin({top: 1, right:0, left:24, bottom: 0});
adjustGraphSize(job_status_chart, element);
}
function onResize() {

View File

@ -14,13 +14,16 @@ export default
function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q) {
function pluck(property, promise) {
function pluck(property, promise, status) {
return promise.then(function(value) {
if(status === "successful" || status === "failed"){
delete value[property].jobs[status];
}
return value[property];
});
}
function getData(period, jobType) {
function getData(period, jobType, status) {
var url, dash_path = getBasePath('dashboard');
if(dash_path === '' ){
processErrors(null,
@ -48,7 +51,7 @@ function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q) {
return $q.reject(response);
});
return pluck('data', result);
return pluck('data', result, status);
}
return {
@ -64,12 +67,12 @@ function JobStatusGraphData(Rest, getBasePath, processErrors, $rootScope, $q) {
});
});
},
get: function(period, jobType) {
get: function(period, jobType, status) {
this.destroyWatcher();
this.setupWatcher(period, jobType);
return getData(period, jobType);
return getData(period, jobType, status);
}
};

View File

@ -1,46 +1 @@
<div class="DashboardGraphs-graphToolbar">
<div class="DashboardGraphs-filterDropdown">
Period:
<a id="period-dropdown" role="button" data-toggle="dropdown" data-target="#" href="/page.html">
Past Month<span class="caret"></span>
</a>
<ul class="dropdown-menu DashboardGraphs-filterDropdownItems
DashboardGraphs-filterDropdownItems--period" role="menu" aria-labelledby="period-dropdown">
<li>
<a class="n" id="day" >Past 24 Hours </a>
</li>
<li>
<a class="n" id="week">Past Week</a>
</li>
<li>
<a class="n" id="month">Past Month</a>
</li>
</ul>
</div>
<div class="DashboardGraphs-filterDropdown">
Job Type:
<a id="type-dropdown" role="button" data-toggle="dropdown" data-target="#" href="/page.html">
All<span class="caret"></span>
</a>
<ul class="dropdown-menu DashboardGraphs-filterDropdownItems
DashboardGraphs-filterDropdownItems--jobType" role="menu" aria-labelledby="type-dropdown">
<li>
<a class="m" id="all">All</a>
</li>
<li>
<a class="m" id="inv_sync">Inventory Sync</a>
</li>
<li>
<a class="m" id="scm_update">SCM Update</a>
</li>
<li>
<a class="m" id="playbook_run">Playbook Run</a>
</li>
</ul>
</div>
</div>
<svg width="100%" height="100%"></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 39 B

View File

@ -2,7 +2,7 @@
.Footer {
height: 40px;
background-color: #d7d7d7;
background-color: #f6f6f6;
color: #848992;
width: 100%;
z-index: 1040;

View File

@ -62,7 +62,6 @@ export default
// if the user clicks outside of the mobile menu,
// close it if it's open
$("body").on('click', function(e) {
e.stopPropagation();
if ($(e.target).parents(".MainMenu").length === 0) {
scope.isHiddenOnMobile = true;
}

View File

@ -26,7 +26,7 @@
@tip-background: #0088CC;
@tip-color: #fff;
@green: #60D66F;
@green: #5bbdbf;
@red: #ff5850;
@red-hover: #FA8C87;
@red-focus: #FF1105;

View File

@ -50,9 +50,6 @@
</div>
</div>
<!-- Password Dialog -->
<div id="password-modal" style="display: none;"></div>
<div id="idle-modal" style="display:none">Your session will expire in <span id="remaining_seconds">60</span> seconds, would you like to continue?</div>