index.tsx 11 KB

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