index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ILabShell,
  5. ILayoutRestorer,
  6. JupyterFrontEnd,
  7. JupyterFrontEndPlugin
  8. } from '@jupyterlab/application';
  9. import {
  10. CommandToolbarButton,
  11. ICommandPalette,
  12. MainAreaWidget,
  13. WidgetTracker,
  14. ReactWidget
  15. } from '@jupyterlab/apputils';
  16. import { IChangedArgs } from '@jupyterlab/coreutils';
  17. import {
  18. ILoggerRegistry,
  19. LogConsolePanel,
  20. LoggerRegistry,
  21. LogLevel
  22. } from '@jupyterlab/logconsole';
  23. import { IMainMenu } from '@jupyterlab/mainmenu';
  24. import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
  25. import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
  26. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  27. import { IStatusBar } from '@jupyterlab/statusbar';
  28. import { HTMLSelect, listIcon } from '@jupyterlab/ui-components';
  29. import { UUID } from '@lumino/coreutils';
  30. import { DockLayout, Widget } from '@lumino/widgets';
  31. import * as React from 'react';
  32. import { logNotebookOutput } from './nboutput';
  33. import { LogConsoleStatus } from './status';
  34. const LOG_CONSOLE_PLUGIN_ID = '@jupyterlab/logconsole-extension:plugin';
  35. /**
  36. * The command IDs used by the plugin.
  37. */
  38. namespace CommandIDs {
  39. export const addCheckpoint = 'logconsole:add-checkpoint';
  40. export const clear = 'logconsole:clear';
  41. export const open = 'logconsole:open';
  42. export const setLevel = 'logconsole:set-level';
  43. }
  44. /**
  45. * The Log Console extension.
  46. */
  47. const logConsolePlugin: JupyterFrontEndPlugin<ILoggerRegistry> = {
  48. activate: activateLogConsole,
  49. id: LOG_CONSOLE_PLUGIN_ID,
  50. provides: ILoggerRegistry,
  51. requires: [ILabShell, IRenderMimeRegistry, INotebookTracker],
  52. optional: [
  53. ICommandPalette,
  54. ILayoutRestorer,
  55. IMainMenu,
  56. ISettingRegistry,
  57. IStatusBar
  58. ],
  59. autoStart: true
  60. };
  61. /**
  62. * Activate the Log Console extension.
  63. */
  64. function activateLogConsole(
  65. app: JupyterFrontEnd,
  66. labShell: ILabShell,
  67. rendermime: IRenderMimeRegistry,
  68. nbtracker: INotebookTracker,
  69. palette: ICommandPalette | null,
  70. restorer: ILayoutRestorer | null,
  71. mainMenu: IMainMenu | null,
  72. settingRegistry: ISettingRegistry | null,
  73. statusBar: IStatusBar | null
  74. ): ILoggerRegistry {
  75. let logConsoleWidget: MainAreaWidget<LogConsolePanel> | null = null;
  76. let logConsolePanel: LogConsolePanel | null = null;
  77. const loggerRegistry = new LoggerRegistry({
  78. defaultRendermime: rendermime,
  79. // The maxLength is reset below from settings
  80. maxLength: 1000
  81. });
  82. const tracker = new WidgetTracker<MainAreaWidget<LogConsolePanel>>({
  83. namespace: 'logconsole'
  84. });
  85. if (restorer) {
  86. void restorer.restore(tracker, {
  87. command: CommandIDs.open,
  88. name: () => 'logconsole'
  89. });
  90. }
  91. const status = new LogConsoleStatus({
  92. loggerRegistry: loggerRegistry,
  93. handleClick: () => {
  94. if (!logConsoleWidget) {
  95. createLogConsoleWidget({
  96. insertMode: 'split-bottom',
  97. ref: app.shell.currentWidget?.id
  98. });
  99. } else {
  100. app.shell.activateById(logConsoleWidget.id);
  101. }
  102. }
  103. });
  104. interface ILogConsoleOptions {
  105. source?: string;
  106. insertMode?: DockLayout.InsertMode;
  107. ref?: string;
  108. }
  109. const createLogConsoleWidget = (options: ILogConsoleOptions = {}) => {
  110. logConsolePanel = new LogConsolePanel(loggerRegistry);
  111. logConsolePanel.source =
  112. options.source !== undefined
  113. ? options.source
  114. : nbtracker.currentWidget
  115. ? nbtracker.currentWidget.context.path
  116. : null;
  117. logConsoleWidget = new MainAreaWidget({ content: logConsolePanel });
  118. logConsoleWidget.addClass('jp-LogConsole');
  119. logConsoleWidget.title.closable = true;
  120. logConsoleWidget.title.label = 'Log Console';
  121. logConsoleWidget.title.iconRenderer = listIcon;
  122. const addCheckpointButton = new CommandToolbarButton({
  123. commands: app.commands,
  124. id: CommandIDs.addCheckpoint
  125. });
  126. const clearButton = new CommandToolbarButton({
  127. commands: app.commands,
  128. id: CommandIDs.clear
  129. });
  130. logConsoleWidget.toolbar.addItem(
  131. 'lab-log-console-add-checkpoint',
  132. addCheckpointButton
  133. );
  134. logConsoleWidget.toolbar.addItem('lab-log-console-clear', clearButton);
  135. logConsoleWidget.toolbar.addItem(
  136. 'level',
  137. new LogLevelSwitcher(logConsoleWidget.content)
  138. );
  139. logConsolePanel.sourceChanged.connect(() => {
  140. app.commands.notifyCommandChanged();
  141. });
  142. logConsolePanel.sourceDisplayed.connect((panel, { source, version }) => {
  143. status.model.sourceDisplayed(source, version);
  144. });
  145. logConsoleWidget.disposed.connect(() => {
  146. logConsoleWidget = null;
  147. logConsolePanel = null;
  148. app.commands.notifyCommandChanged();
  149. });
  150. app.shell.add(logConsoleWidget, 'main', {
  151. ref: options.ref,
  152. mode: options.insertMode
  153. });
  154. void tracker.add(logConsoleWidget);
  155. logConsoleWidget.update();
  156. app.commands.notifyCommandChanged();
  157. };
  158. app.commands.addCommand(CommandIDs.open, {
  159. label: 'Show Log Console',
  160. execute: (options: ILogConsoleOptions = {}) => {
  161. // Toggle the display
  162. if (logConsoleWidget) {
  163. logConsoleWidget.dispose();
  164. } else {
  165. createLogConsoleWidget(options);
  166. }
  167. },
  168. isToggled: () => {
  169. return logConsoleWidget !== null;
  170. }
  171. });
  172. app.commands.addCommand(CommandIDs.addCheckpoint, {
  173. label: 'Add Checkpoint',
  174. execute: () => {
  175. logConsolePanel?.logger?.checkpoint();
  176. },
  177. isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null,
  178. iconClass: 'jp-AddIcon'
  179. });
  180. app.commands.addCommand(CommandIDs.clear, {
  181. label: 'Clear Log',
  182. execute: () => {
  183. logConsolePanel?.logger?.clear();
  184. },
  185. isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null,
  186. // TODO: figure out how this jp-clearIcon class should work, analagous to jp-AddIcon
  187. iconClass: 'fa fa-ban jp-ClearIcon'
  188. });
  189. function toTitleCase(value: string) {
  190. return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
  191. }
  192. app.commands.addCommand(CommandIDs.setLevel, {
  193. label: args => `Set Log Level to ${toTitleCase(args.level as string)}`,
  194. execute: (args: { level: LogLevel }) => {
  195. if (logConsolePanel?.logger) {
  196. logConsolePanel.logger.level = args.level;
  197. }
  198. },
  199. isEnabled: () => !!logConsolePanel && logConsolePanel.source !== null
  200. // TODO: find good icon class
  201. });
  202. app.contextMenu.addItem({
  203. command: CommandIDs.open,
  204. selector: '.jp-Notebook'
  205. });
  206. if (mainMenu) {
  207. mainMenu.viewMenu.addGroup([{ command: CommandIDs.open }]);
  208. }
  209. if (palette) {
  210. palette.addItem({ command: CommandIDs.open, category: 'Main Area' });
  211. }
  212. if (statusBar) {
  213. statusBar.registerStatusItem('@jupyterlab/logconsole-extension:status', {
  214. item: status,
  215. align: 'left',
  216. isActive: () => true,
  217. activeStateChanged: status.model!.stateChanged
  218. });
  219. }
  220. function setSource(newValue: Widget | null) {
  221. if (logConsoleWidget && newValue === logConsoleWidget) {
  222. // Do not change anything if we are just focusing on ourselves
  223. return;
  224. }
  225. let source: string | null;
  226. if (newValue && nbtracker.has(newValue)) {
  227. source = (newValue as NotebookPanel).context.path;
  228. } else {
  229. source = null;
  230. }
  231. if (logConsolePanel) {
  232. logConsolePanel.source = source;
  233. }
  234. status.model.source = source;
  235. }
  236. void app.restored.then(() => {
  237. // Set source only after app is restored in order to allow restorer to
  238. // restore previous source first, which may set the renderer
  239. setSource(labShell.currentWidget);
  240. labShell.currentChanged.connect((_, { newValue }) => setSource(newValue));
  241. });
  242. if (settingRegistry) {
  243. const updateSettings = (settings: ISettingRegistry.ISettings): void => {
  244. loggerRegistry.maxLength = settings.get('maxLogEntries')
  245. .composite as number;
  246. status.model.flashEnabled = settings.get('flash').composite as boolean;
  247. };
  248. Promise.all([settingRegistry.load(LOG_CONSOLE_PLUGIN_ID), app.restored])
  249. .then(([settings]) => {
  250. updateSettings(settings);
  251. settings.changed.connect(settings => {
  252. updateSettings(settings);
  253. });
  254. })
  255. .catch((reason: Error) => {
  256. console.error(reason.message);
  257. });
  258. }
  259. return loggerRegistry;
  260. }
  261. /**
  262. * A toolbar widget that switches log levels.
  263. */
  264. export class LogLevelSwitcher extends ReactWidget {
  265. /**
  266. * Construct a new cell type switcher.
  267. */
  268. constructor(widget: LogConsolePanel) {
  269. super();
  270. this.addClass('jp-LogConsole-toolbarLogLevel');
  271. this._logConsole = widget;
  272. if (widget.source) {
  273. this.update();
  274. }
  275. widget.sourceChanged.connect(this._updateSource, this);
  276. }
  277. private _updateSource(
  278. sender: LogConsolePanel,
  279. { oldValue, newValue }: IChangedArgs<string | null>
  280. ) {
  281. // Transfer stateChanged handler to new source logger
  282. if (oldValue !== null) {
  283. const logger = sender.loggerRegistry.getLogger(oldValue);
  284. logger.stateChanged.disconnect(this.update, this);
  285. }
  286. if (newValue !== null) {
  287. const logger = sender.loggerRegistry.getLogger(newValue);
  288. logger.stateChanged.connect(this.update, this);
  289. }
  290. this.update();
  291. }
  292. /**
  293. * Handle `change` events for the HTMLSelect component.
  294. */
  295. handleChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
  296. if (this._logConsole.logger) {
  297. this._logConsole.logger.level = event.target.value as LogLevel;
  298. }
  299. this.update();
  300. };
  301. /**
  302. * Handle `keydown` events for the HTMLSelect component.
  303. */
  304. handleKeyDown = (event: React.KeyboardEvent): void => {
  305. if (event.keyCode === 13) {
  306. this._logConsole.activate();
  307. }
  308. };
  309. render() {
  310. let logger = this._logConsole.logger;
  311. return (
  312. <>
  313. <label
  314. htmlFor={this._id}
  315. className={
  316. logger === null
  317. ? 'jp-LogConsole-toolbarLogLevel-disabled'
  318. : undefined
  319. }
  320. >
  321. Log Level:
  322. </label>
  323. <HTMLSelect
  324. id={this._id}
  325. className="jp-LogConsole-toolbarLogLevelDropdown"
  326. onChange={this.handleChange}
  327. onKeyDown={this.handleKeyDown}
  328. value={logger?.level}
  329. iconProps={{
  330. icon: <span className="jp-MaterialIcon jp-DownCaretIcon bp3-icon" />
  331. }}
  332. aria-label="Log level"
  333. minimal
  334. disabled={logger === null}
  335. options={
  336. logger === null
  337. ? []
  338. : [
  339. 'Critical',
  340. 'Error',
  341. 'Warning',
  342. 'Info',
  343. 'Debug'
  344. ].map(label => ({ label, value: label.toLowerCase() }))
  345. }
  346. />
  347. </>
  348. );
  349. }
  350. private _logConsole: LogConsolePanel;
  351. private _id = `level-${UUID.uuid4()}`;
  352. }
  353. export default [logConsolePlugin, logNotebookOutput];