| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library logging_page; |
| |
| import 'dart:async'; |
| import 'dart:html'; |
| import 'observatory_element.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:observatory/service.dart'; |
| import 'package:observatory/app.dart'; |
| import 'package:observatory/elements.dart'; |
| import 'package:observatory/utils.dart'; |
| import 'package:polymer/polymer.dart'; |
| |
| @CustomTag('logging-page') |
| class LoggingPageElement extends ObservatoryElement { |
| static const _kPageSelector = '#page'; |
| static const _kLogSelector = '#log'; |
| static const _kSeverityLevelSelector = '#severityLevelSelector'; |
| |
| LoggingPageElement.created() : super.created(); |
| |
| domReady() { |
| super.domReady(); |
| _insertLevels(); |
| } |
| |
| attached() { |
| super.attached(); |
| _sub(); |
| _resizeSubscription = window.onResize.listen((_) => _updatePageHeight()); |
| _updatePageHeight(); |
| // Turn on the periodic poll timer for this page. |
| pollPeriod = const Duration(milliseconds:100); |
| } |
| |
| detached() { |
| super.detached(); |
| if (_resizeSubscription != null) { |
| _resizeSubscription.cancel(); |
| _resizeSubscription = null; |
| } |
| _unsub(); |
| } |
| |
| void onPoll() { |
| _flushPendingLogs(); |
| } |
| |
| _updatePageHeight() { |
| HtmlElement e = shadowRoot.querySelector(_kPageSelector); |
| final totalHeight = window.innerHeight; |
| final top = e.offset.top; |
| final bottomMargin = 32; |
| final mainHeight = totalHeight - top - bottomMargin; |
| e.style.setProperty('height', '${mainHeight}px'); |
| } |
| |
| _insertLevels() { |
| SelectElement severityLevelSelector = |
| shadowRoot.querySelector(_kSeverityLevelSelector); |
| severityLevelSelector.children.clear(); |
| _maxLevelLabelLength = 0; |
| for (var level in Level.LEVELS) { |
| var option = new OptionElement(); |
| option.value = level.value.toString(); |
| option.label = level.name; |
| if (level.name.length > _maxLevelLabelLength) { |
| _maxLevelLabelLength = level.name.length; |
| } |
| severityLevelSelector.children.add(option); |
| } |
| severityLevelSelector.selectedIndex = 0; |
| severityLevel = Level.ALL.value.toString(); |
| } |
| |
| _reset() { |
| logRecords.clear(); |
| _unsub(); |
| _sub(); |
| _renderFull(); |
| } |
| |
| _unsub() { |
| cancelFutureSubscription(_loggingSubscriptionFuture); |
| _loggingSubscriptionFuture = null; |
| } |
| |
| _sub() { |
| if (_loggingSubscriptionFuture != null) { |
| // Already subscribed. |
| return; |
| } |
| _loggingSubscriptionFuture = |
| app.vm.listenEventStream(Isolate.kLoggingStream, _onEvent); |
| } |
| |
| _append(Map logRecord) { |
| logRecords.add(logRecord); |
| if (_shouldDisplay(logRecord)) { |
| // Queue for display. |
| pendingLogRecords.add(logRecord); |
| } |
| } |
| |
| Element _renderAppend(Map logRecord) { |
| DivElement logContainer = shadowRoot.querySelector(_kLogSelector); |
| var element = new DivElement(); |
| element.classes.add('logItem'); |
| element.classes.add(logRecord['level'].name); |
| element.appendText( |
| '${logRecord["level"].name.padLeft(_maxLevelLabelLength)} ' |
| '${Utils.formatDateTime(logRecord["time"])} ' |
| '${logRecord["message"].valueAsString}\n'); |
| logContainer.children.add(element); |
| return element; |
| } |
| |
| _renderFull() { |
| DivElement logContainer = shadowRoot.querySelector(_kLogSelector); |
| logContainer.children.clear(); |
| pendingLogRecords.clear(); |
| for (var logRecord in logRecords) { |
| if (_shouldDisplay(logRecord)) { |
| _renderAppend(logRecord); |
| } |
| } |
| _scrollToBottom(logContainer); |
| } |
| |
| /// Is [container] scrolled to the within [threshold] pixels of the bottom? |
| static bool _isScrolledToBottom(DivElement container, [int threshold = 2]) { |
| if (container == null) { |
| return false; |
| } |
| // scrollHeight -> complete height of element including scrollable area. |
| // clientHeight -> height of element on page. |
| // scrollTop -> how far is an element scrolled (from 0 to scrollHeight). |
| final distanceFromBottom = |
| container.scrollHeight - container.clientHeight - container.scrollTop; |
| const threshold = 2; // 2 pixel slop. |
| return distanceFromBottom <= threshold; |
| } |
| |
| /// Scroll [container] so the bottom content is visible. |
| static _scrollToBottom(DivElement container) { |
| if (container == null) { |
| return; |
| } |
| // Adjust scroll so that the bottom of the content is visible. |
| container.scrollTop = container.scrollHeight - container.clientHeight; |
| } |
| |
| _flushPendingLogs() { |
| DivElement logContainer = shadowRoot.querySelector(_kLogSelector); |
| bool autoScroll = _isScrolledToBottom(logContainer); |
| for (var logRecord in pendingLogRecords) { |
| _renderAppend(logRecord); |
| } |
| if (autoScroll) { |
| _scrollToBottom(logContainer); |
| } |
| pendingLogRecords.clear(); |
| } |
| |
| _onEvent(ServiceEvent event) { |
| assert(event.kind == Isolate.kLoggingStream); |
| _append(event.logRecord); |
| } |
| |
| void isolateChanged(oldValue) { |
| _reset(); |
| } |
| |
| void severityLevelChanged(oldValue) { |
| _severityLevelValue = int.parse(severityLevel); |
| _renderFull(); |
| } |
| |
| Future clear() { |
| logRecords.clear(); |
| pendingLogRecords.clear(); |
| _renderFull(); |
| return new Future.value(null); |
| } |
| |
| bool _shouldDisplay(Map logRecord) { |
| return logRecord['level'].value >= _severityLevelValue; |
| } |
| |
| @observable Isolate isolate; |
| @observable String severityLevel; |
| int _severityLevelValue = 0; |
| int _maxLevelLabelLength = 0; |
| Future<StreamSubscription> _loggingSubscriptionFuture; |
| StreamSubscription _resizeSubscription; |
| final List<Map> logRecords = new List<Map>(); |
| final List<Map> pendingLogRecords = new List<Map>(); |
| } |