index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JupyterFrontEnd,
  5. JupyterFrontEndPlugin,
  6. ILayoutRestorer
  7. } from '@jupyterlab/application';
  8. import {
  9. MainAreaWidget,
  10. WidgetTracker,
  11. ToolbarButton
  12. } from '@jupyterlab/apputils';
  13. import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
  14. import {
  15. ILoggerRegistry,
  16. LoggerRegistry,
  17. LogConsolePanel,
  18. ILogger,
  19. ILoggerChange,
  20. ILoggerRegistryChange,
  21. DEFAULT_LOG_ENTRY_LIMIT
  22. } from '@jupyterlab/logconsole';
  23. import { ICommandPalette, VDomModel, VDomRenderer } from '@jupyterlab/apputils';
  24. import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
  25. import { IMainMenu } from '@jupyterlab/mainmenu';
  26. import React from 'react';
  27. import {
  28. IStatusBar,
  29. GroupItem,
  30. IconItem,
  31. TextItem,
  32. interactiveItem
  33. } from '@jupyterlab/statusbar';
  34. import { ISettingRegistry } from '@jupyterlab/coreutils';
  35. import { Signal } from '@phosphor/signaling';
  36. const LOG_CONSOLE_PLUGIN_ID = '@jupyterlab/logconsole-extension:plugin';
  37. /**
  38. * The Log Console extension.
  39. */
  40. const logConsolePlugin: JupyterFrontEndPlugin<ILoggerRegistry> = {
  41. activate: activateLogConsole,
  42. id: LOG_CONSOLE_PLUGIN_ID,
  43. provides: ILoggerRegistry,
  44. requires: [
  45. IMainMenu,
  46. ICommandPalette,
  47. INotebookTracker,
  48. IStatusBar,
  49. IRenderMimeRegistry
  50. ],
  51. optional: [ILayoutRestorer, ISettingRegistry],
  52. autoStart: true
  53. };
  54. /*
  55. * A namespace for LogConsoleStatusComponent.
  56. */
  57. namespace LogConsoleStatusComponent {
  58. /**
  59. * The props for the LogConsoleStatusComponent.
  60. */
  61. export interface IProps {
  62. /**
  63. * A click handler for the item. By default
  64. * Log Console panel is launched.
  65. */
  66. handleClick: () => void;
  67. /**
  68. * Number of logs.
  69. */
  70. logCount: number;
  71. }
  72. }
  73. /**
  74. * A pure functional component for a Log Console status item.
  75. *
  76. * @param props - the props for the component.
  77. *
  78. * @returns a tsx component for rendering the Log Console status.
  79. */
  80. function LogConsoleStatusComponent(
  81. props: LogConsoleStatusComponent.IProps
  82. ): React.ReactElement<LogConsoleStatusComponent.IProps> {
  83. return (
  84. <GroupItem
  85. spacing={0}
  86. onClick={props.handleClick}
  87. title={`${props.logCount} logs in Log Console`}
  88. >
  89. <IconItem source={'jp-LogConsoleIcon'} />
  90. <TextItem source={props.logCount} />
  91. </GroupItem>
  92. );
  93. }
  94. /**
  95. * A VDomRenderer widget for displaying the status of Log Console logs.
  96. */
  97. export class LogConsoleStatus extends VDomRenderer<LogConsoleStatus.Model> {
  98. /**
  99. * Construct the log console status widget.
  100. *
  101. * @param options - The status widget initialization options.
  102. */
  103. constructor(options: LogConsoleStatus.IOptions) {
  104. super();
  105. this._handleClick = options.handleClick;
  106. this.model = new LogConsoleStatus.Model(options.loggerRegistry);
  107. this.addClass(interactiveItem);
  108. this.addClass('jp-LogConsoleStatusItem');
  109. let flashRequestTimer: number = null;
  110. this.model.activeSourceChanged.connect(() => {
  111. if (
  112. this.model.activeSource &&
  113. this.model.flashEnabled &&
  114. !this.model.isSourceLogsViewed(this.model.activeSource) &&
  115. this.model.logCount > 0
  116. ) {
  117. this._showHighlighted();
  118. } else {
  119. this._clearHighlight();
  120. }
  121. });
  122. this.model.flashEnabledChanged.connect(() => {
  123. if (!this.model.flashEnabled) {
  124. this._clearHighlight();
  125. }
  126. });
  127. this.model.logChanged.connect(() => {
  128. if (!this.model.flashEnabled || this.model.logCount === 0) {
  129. // cancel existing request
  130. clearTimeout(flashRequestTimer);
  131. flashRequestTimer = null;
  132. this._clearHighlight();
  133. return;
  134. }
  135. const wasFlashed = this.hasClass('hilite') || this.hasClass('hilited');
  136. if (wasFlashed) {
  137. this._clearHighlight();
  138. // cancel previous request
  139. clearTimeout(flashRequestTimer);
  140. flashRequestTimer = setTimeout(() => {
  141. this._flashHighlight();
  142. }, 100);
  143. } else {
  144. this._flashHighlight();
  145. }
  146. });
  147. }
  148. /**
  149. * Render the log console status item.
  150. */
  151. render() {
  152. if (this.model === null) {
  153. return null;
  154. } else {
  155. return (
  156. <LogConsoleStatusComponent
  157. handleClick={this._handleClick}
  158. logCount={this.model.logCount}
  159. />
  160. );
  161. }
  162. }
  163. private _flashHighlight() {
  164. this.addClass('hilite');
  165. }
  166. private _showHighlighted() {
  167. this.addClass('hilited');
  168. }
  169. private _clearHighlight() {
  170. this.removeClass('hilite');
  171. this.removeClass('hilited');
  172. }
  173. private _handleClick: () => void;
  174. }
  175. /**
  176. * A namespace for Log Console log status.
  177. */
  178. export namespace LogConsoleStatus {
  179. /**
  180. * A VDomModel for the LogConsoleStatus item.
  181. */
  182. export class Model extends VDomModel {
  183. /**
  184. * Create a new LogConsoleStatus model.
  185. *
  186. * @param loggerRegistry - The logger registry providing the logs.
  187. */
  188. constructor(loggerRegistry: ILoggerRegistry) {
  189. super();
  190. this._loggerRegistry = loggerRegistry;
  191. this._loggerRegistry.registryChanged.connect(
  192. (sender: ILoggerRegistry, args: ILoggerRegistryChange) => {
  193. const loggers = this._loggerRegistry.getLoggers();
  194. for (let logger of loggers) {
  195. if (this._loggersWatched.has(logger.source)) {
  196. continue;
  197. }
  198. logger.logChanged.connect(
  199. (sender: ILogger, change: ILoggerChange) => {
  200. if (sender.source === this._activeSource) {
  201. this.stateChanged.emit();
  202. this.logChanged.emit();
  203. }
  204. // mark logger as dirty
  205. this._loggersWatched.set(sender.source, false);
  206. }
  207. );
  208. // mark logger as viewed
  209. this._loggersWatched.set(logger.source, true);
  210. }
  211. }
  212. );
  213. }
  214. /**
  215. * Number of logs.
  216. */
  217. get logCount(): number {
  218. if (this._activeSource) {
  219. const logger = this._loggerRegistry.getLogger(this._activeSource);
  220. return Math.min(logger.length, this._entryLimit);
  221. }
  222. return 0;
  223. }
  224. /**
  225. * The name of the active log source
  226. */
  227. get activeSource(): string {
  228. return this._activeSource;
  229. }
  230. set activeSource(name: string) {
  231. if (this._activeSource === name) {
  232. return;
  233. }
  234. this._activeSource = name;
  235. this.activeSourceChanged.emit();
  236. // refresh rendering
  237. this.stateChanged.emit();
  238. }
  239. /**
  240. * Flag to toggle flashing when new logs added.
  241. */
  242. get flashEnabled(): boolean {
  243. return this._flashEnabled;
  244. }
  245. set flashEnabled(enabled: boolean) {
  246. if (this._flashEnabled === enabled) {
  247. return;
  248. }
  249. this._flashEnabled = enabled;
  250. this.flashEnabledChanged.emit();
  251. // refresh rendering
  252. this.stateChanged.emit();
  253. }
  254. /**
  255. * Log output entry limit.
  256. */
  257. set entryLimit(limit: number) {
  258. if (limit > 0) {
  259. this._entryLimit = limit;
  260. // refresh rendering
  261. this.stateChanged.emit();
  262. }
  263. }
  264. /**
  265. * Mark logs from the source as viewed.
  266. *
  267. * @param source - The name of the log source.
  268. */
  269. markSourceLogsViewed(source: string) {
  270. this._loggersWatched.set(source, true);
  271. }
  272. /**
  273. * Check if logs from the source are viewed.
  274. *
  275. * @param source - The name of the log source.
  276. *
  277. * @returns True if logs from source are viewer.
  278. */
  279. isSourceLogsViewed(source: string): boolean {
  280. return (
  281. !this._loggersWatched.has(source) ||
  282. this._loggersWatched.get(source) === true
  283. );
  284. }
  285. /**
  286. * A signal emitted when the log model changes.
  287. */
  288. public logChanged = new Signal<this, void>(this);
  289. /**
  290. * A signal emitted when the active log source changes.
  291. */
  292. public activeSourceChanged = new Signal<this, void>(this);
  293. /**
  294. * A signal emitted when the flash enablement changes.
  295. */
  296. public flashEnabledChanged = new Signal<this, void>(this);
  297. private _flashEnabled: boolean = true;
  298. private _loggerRegistry: ILoggerRegistry;
  299. private _activeSource: string = null;
  300. private _entryLimit: number = DEFAULT_LOG_ENTRY_LIMIT;
  301. // A map storing keys as source names of the loggers watched
  302. // and values as whether logs from the source are viewed
  303. private _loggersWatched: Map<string, boolean> = new Map();
  304. }
  305. /**
  306. * Options for creating a new LogConsoleStatus item
  307. */
  308. export interface IOptions {
  309. /**
  310. * The logger registry providing the logs.
  311. */
  312. loggerRegistry: ILoggerRegistry;
  313. /**
  314. * A click handler for the item. By default
  315. * Log Console panel is launched.
  316. */
  317. handleClick: () => void;
  318. }
  319. }
  320. /**
  321. * Activate the Log Console extension.
  322. */
  323. function activateLogConsole(
  324. app: JupyterFrontEnd,
  325. mainMenu: IMainMenu,
  326. palette: ICommandPalette,
  327. nbtracker: INotebookTracker,
  328. statusBar: IStatusBar,
  329. rendermime: IRenderMimeRegistry,
  330. restorer: ILayoutRestorer | null,
  331. settingRegistry: ISettingRegistry | null
  332. ): ILoggerRegistry {
  333. let logConsoleWidget: MainAreaWidget<LogConsolePanel> = null;
  334. let entryLimit: number = DEFAULT_LOG_ENTRY_LIMIT;
  335. let flashEnabled: boolean = true;
  336. const loggerRegistry = new LoggerRegistry(rendermime);
  337. const command = 'logconsole:open';
  338. const category: string = 'Main Area';
  339. const tracker = new WidgetTracker<MainAreaWidget<LogConsolePanel>>({
  340. namespace: 'logconsole'
  341. });
  342. if (restorer) {
  343. void restorer.restore(tracker, {
  344. command,
  345. args: obj => ({
  346. fromRestorer: true,
  347. activeSource: obj.content.activeSource
  348. }),
  349. name: () => 'logconsole'
  350. });
  351. }
  352. const status = new LogConsoleStatus({
  353. loggerRegistry: loggerRegistry,
  354. handleClick: () => {
  355. if (!logConsoleWidget) {
  356. createLogConsoleWidget();
  357. } else {
  358. logConsoleWidget.activate();
  359. }
  360. }
  361. });
  362. const createLogConsoleWidget = () => {
  363. let activeSource: string = nbtracker.currentWidget
  364. ? nbtracker.currentWidget.context.path
  365. : null;
  366. const logConsolePanel = new LogConsolePanel(loggerRegistry);
  367. logConsoleWidget = new MainAreaWidget({ content: logConsolePanel });
  368. logConsoleWidget.addClass('jp-LogConsole');
  369. logConsoleWidget.title.closable = true;
  370. logConsoleWidget.title.label = 'Log Console';
  371. logConsoleWidget.title.iconClass = 'jp-LogConsoleIcon';
  372. logConsolePanel.entryLimit = entryLimit;
  373. const addTimestampButton = new ToolbarButton({
  374. onClick: (): void => {
  375. if (!logConsolePanel.activeSource) {
  376. return;
  377. }
  378. const logger = loggerRegistry.getLogger(logConsolePanel.activeSource);
  379. logger.log({
  380. data: {
  381. 'text/html': '<hr>'
  382. },
  383. output_type: 'display_data'
  384. });
  385. },
  386. iconClassName: 'jp-AddIcon',
  387. tooltip: 'Add Timestamp',
  388. label: 'Add Timestamp'
  389. });
  390. const clearButton = new ToolbarButton({
  391. onClick: (): void => {
  392. const logger = loggerRegistry.getLogger(logConsolePanel.activeSource);
  393. logger.clear();
  394. },
  395. iconClassName: 'fa fa-ban clear-icon',
  396. tooltip: 'Clear Logs',
  397. label: 'Clear Logs'
  398. });
  399. logConsoleWidget.toolbar.addItem(
  400. 'lab-output-console-add-timestamp',
  401. addTimestampButton
  402. );
  403. logConsoleWidget.toolbar.addItem('lab-output-console-clear', clearButton);
  404. void tracker.add(logConsoleWidget);
  405. logConsolePanel.attached.connect(() => {
  406. status.model.markSourceLogsViewed(status.model.activeSource);
  407. status.model.flashEnabled = false;
  408. });
  409. logConsoleWidget.disposed.connect(() => {
  410. logConsoleWidget = null;
  411. status.model.flashEnabled = flashEnabled;
  412. });
  413. app.shell.add(logConsoleWidget, 'main', {
  414. ref: '',
  415. mode: 'split-bottom'
  416. });
  417. logConsoleWidget.update();
  418. app.shell.activateById(logConsoleWidget.id);
  419. if (activeSource) {
  420. logConsolePanel.activeSource = activeSource;
  421. }
  422. };
  423. app.commands.addCommand(command, {
  424. label: 'Show Log Console',
  425. execute: (args: any) => {
  426. if (!logConsoleWidget) {
  427. createLogConsoleWidget();
  428. if (args && args.activeSource) {
  429. logConsoleWidget.content.activeSource = args.activeSource;
  430. }
  431. } else if (!(args && args.fromRestorer)) {
  432. logConsoleWidget.dispose();
  433. }
  434. },
  435. isToggled: () => {
  436. return logConsoleWidget !== null;
  437. }
  438. });
  439. mainMenu.viewMenu.addGroup([{ command }]);
  440. palette.addItem({ command, category });
  441. app.contextMenu.addItem({
  442. command: command,
  443. selector: '.jp-Notebook'
  444. });
  445. let appRestored = false;
  446. void app.restored.then(() => {
  447. appRestored = true;
  448. });
  449. statusBar.registerStatusItem('@jupyterlab/logconsole-extension:status', {
  450. item: status,
  451. align: 'left',
  452. isActive: () => true,
  453. activeStateChanged: status.model!.stateChanged
  454. });
  455. nbtracker.widgetAdded.connect(
  456. (sender: INotebookTracker, nb: NotebookPanel) => {
  457. nb.activated.connect((nb: NotebookPanel, args: void) => {
  458. // set activeSource only after app is restored
  459. // in order to allow restorer to restore previous activeSource
  460. if (!appRestored) {
  461. return;
  462. }
  463. const sourceName = nb.context.path;
  464. if (logConsoleWidget) {
  465. logConsoleWidget.content.activeSource = sourceName;
  466. status.model.markSourceLogsViewed(sourceName);
  467. void tracker.save(logConsoleWidget);
  468. }
  469. status.model.activeSource = sourceName;
  470. });
  471. nb.disposed.connect((nb: NotebookPanel, args: void) => {
  472. const sourceName = nb.context.path;
  473. if (
  474. logConsoleWidget &&
  475. logConsoleWidget.content.activeSource === sourceName
  476. ) {
  477. logConsoleWidget.content.activeSource = null;
  478. void tracker.save(logConsoleWidget);
  479. }
  480. if (status.model.activeSource === sourceName) {
  481. status.model.activeSource = null;
  482. }
  483. });
  484. }
  485. );
  486. if (settingRegistry) {
  487. const updateSettings = (settings: ISettingRegistry.ISettings): void => {
  488. const maxLogEntries = settings.get('maxLogEntries').composite as number;
  489. entryLimit = maxLogEntries;
  490. if (logConsoleWidget) {
  491. logConsoleWidget.content.entryLimit = entryLimit;
  492. }
  493. status.model.entryLimit = entryLimit;
  494. flashEnabled = settings.get('flash').composite as boolean;
  495. status.model.flashEnabled = !logConsoleWidget && flashEnabled;
  496. };
  497. Promise.all([settingRegistry.load(LOG_CONSOLE_PLUGIN_ID), app.restored])
  498. .then(([settings]) => {
  499. updateSettings(settings);
  500. settings.changed.connect(settings => {
  501. updateSettings(settings);
  502. });
  503. })
  504. .catch((reason: Error) => {
  505. console.error(reason.message);
  506. });
  507. }
  508. return loggerRegistry;
  509. // The notebook can call this command.
  510. // When is the output model disposed?
  511. }
  512. export default [logConsolePlugin];